Merge "[flexiglass] Add start order dep from SysuiStatusBarStateController on CentralSurfaces" into main
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index 3e5f1cb..fa11278 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -379,6 +379,7 @@
 aconfig_declarations {
     name: "com.android.internal.os.flags-aconfig",
     package: "com.android.internal.os",
+    container: "system",
     srcs: ["core/java/com/android/internal/os/flags.aconfig"],
 }
 
@@ -853,6 +854,7 @@
 aconfig_declarations {
     name: "com.android.server.contextualsearch.flags-aconfig",
     package: "com.android.server.contextualsearch.flags",
+    container: "system",
     srcs: ["services/contextualsearch/flags/flags.aconfig"],
 }
 
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 443a6c0e..5a3ff83 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -263,6 +263,7 @@
     method @RequiresPermission("android.permission.MANAGE_APPOPS") public void setHistoryParameters(int, long, int);
     method @RequiresPermission(android.Manifest.permission.MANAGE_APP_OPS_MODES) public void setMode(int, int, String, int);
     method public static int strOpToOp(@NonNull String);
+    method public int unsafeCheckOpRawNoThrow(@NonNull String, @NonNull android.content.AttributionSource);
     field public static final int ATTRIBUTION_CHAIN_ID_NONE = -1; // 0xffffffff
     field public static final int ATTRIBUTION_FLAGS_NONE = 0; // 0x0
     field public static final int ATTRIBUTION_FLAG_ACCESSOR = 1; // 0x1
diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java
index f4d1304..b3312a8 100644
--- a/core/java/android/app/AppOpsManager.java
+++ b/core/java/android/app/AppOpsManager.java
@@ -8788,6 +8788,18 @@
      * Does not throw a security exception, does not translate {@link #MODE_FOREGROUND}.
      * @hide
      */
+    @TestApi
+    @SuppressLint("UnflaggedApi") // @TestApi without associated feature.
+    public int unsafeCheckOpRawNoThrow(
+            @NonNull String op, @NonNull AttributionSource attributionSource) {
+        return unsafeCheckOpRawNoThrow(strOpToOp(op), attributionSource);
+    }
+
+    /**
+     * Returns the <em>raw</em> mode associated with the op.
+     * Does not throw a security exception, does not translate {@link #MODE_FOREGROUND}.
+     * @hide
+     */
     public int unsafeCheckOpRawNoThrow(int op, int uid, @NonNull String packageName) {
         return unsafeCheckOpRawNoThrow(op, uid, packageName, Context.DEVICE_ID_DEFAULT);
     }
@@ -8798,8 +8810,8 @@
             if (virtualDeviceId == Context.DEVICE_ID_DEFAULT) {
                 return mService.checkOperationRaw(op, uid, packageName, null);
             } else {
-                return mService.checkOperationRawForDevice(op, uid, packageName, null,
-                        Context.DEVICE_ID_DEFAULT);
+                return mService.checkOperationRawForDevice(
+                        op, uid, packageName, null, virtualDeviceId);
             }
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index 436221f..714454b 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -93,7 +93,10 @@
 import android.text.style.CharacterStyle;
 import android.text.style.ForegroundColorSpan;
 import android.text.style.RelativeSizeSpan;
+import android.text.style.StrikethroughSpan;
+import android.text.style.StyleSpan;
 import android.text.style.TextAppearanceSpan;
+import android.text.style.UnderlineSpan;
 import android.util.ArraySet;
 import android.util.Log;
 import android.util.Pair;
@@ -3167,9 +3170,6 @@
                     + " instance is a custom Parcelable and not allowed in Notification");
             return cs.toString();
         }
-        if (Flags.cleanUpSpansAndNewLines()) {
-            return stripStyling(cs);
-        }
 
         return removeTextSizeSpans(cs);
     }
@@ -8285,9 +8285,6 @@
          */
         public BigTextStyle bigText(CharSequence cs) {
             mBigText = safeCharSequence(cs);
-            if (Flags.cleanUpSpansAndNewLines()) {
-                mBigText = cleanUpNewLines(mBigText);
-            }
             return this;
         }
 
@@ -8358,6 +8355,9 @@
 
             // Replace the text with the big text, but only if the big text is not empty.
             CharSequence bigTextText = mBuilder.processLegacyText(mBigText);
+            if (Flags.cleanUpSpansAndNewLines()) {
+                bigTextText = cleanUpNewLines(stripStyling(bigTextText));
+            }
             if (!TextUtils.isEmpty(bigTextText)) {
                 p.text(bigTextText);
             }
@@ -9273,11 +9273,43 @@
              */
             public void ensureColorContrastOrStripStyling(int backgroundColor) {
                 if (Flags.cleanUpSpansAndNewLines()) {
-                    mText = stripStyling(mText);
+                    mText = stripNonStyleSpans(mText);
                 } else {
                     ensureColorContrast(backgroundColor);
                 }
             }
+
+            private CharSequence stripNonStyleSpans(CharSequence text) {
+
+                if (text instanceof Spanned) {
+                    Spanned ss = (Spanned) text;
+                    Object[] spans = ss.getSpans(0, ss.length(), Object.class);
+                    SpannableStringBuilder builder = new SpannableStringBuilder(ss.toString());
+                    for (Object span : spans) {
+                        final Object resultSpan;
+                        if (span instanceof StyleSpan
+                                || span instanceof StrikethroughSpan
+                                || span instanceof UnderlineSpan) {
+                            resultSpan = span;
+                        } else if (span instanceof TextAppearanceSpan) {
+                            final TextAppearanceSpan originalSpan = (TextAppearanceSpan) span;
+                            resultSpan = new TextAppearanceSpan(
+                                    null,
+                                    originalSpan.getTextStyle(),
+                                    -1,
+                                    null,
+                                    null);
+                        } else {
+                            continue;
+                        }
+                        builder.setSpan(resultSpan, ss.getSpanStart(span), ss.getSpanEnd(span),
+                                ss.getSpanFlags(span));
+                    }
+                    return builder;
+                }
+                return text;
+            }
+
             /**
              * Updates TextAppearance spans in the message text so it has sufficient contrast
              * against its background.
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraConfig.java b/core/java/android/companion/virtual/camera/VirtualCameraConfig.java
index 06a0f5c..769b658 100644
--- a/core/java/android/companion/virtual/camera/VirtualCameraConfig.java
+++ b/core/java/android/companion/virtual/camera/VirtualCameraConfig.java
@@ -237,19 +237,15 @@
                 @IntRange(from = 1) int height,
                 @ImageFormat.Format int format,
                 @IntRange(from = 1) int maximumFramesPerSecond) {
-            // TODO(b/310857519): Check dimension upper limits based on the maximum texture size
-            // supported by the current device, instead of hardcoded limits.
-            if (width <= 0 || width > VirtualCameraStreamConfig.DIMENSION_UPPER_LIMIT) {
+            if (width <= 0) {
                 throw new IllegalArgumentException(
                         "Invalid width passed for stream config: " + width
-                                + ", must be between 1 and "
-                                + VirtualCameraStreamConfig.DIMENSION_UPPER_LIMIT);
+                                + ", must be greater than 0");
             }
-            if (height <= 0 || height > VirtualCameraStreamConfig.DIMENSION_UPPER_LIMIT) {
+            if (height <= 0) {
                 throw new IllegalArgumentException(
                         "Invalid height passed for stream config: " + height
-                                + ", must be between 1 and "
-                                + VirtualCameraStreamConfig.DIMENSION_UPPER_LIMIT);
+                                + ", must be greater than 0");
             }
             if (!isFormatSupported(format)) {
                 throw new IllegalArgumentException(
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.java b/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.java
index 00a814e..6ab66b3 100644
--- a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.java
+++ b/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.java
@@ -39,11 +39,6 @@
 public final class VirtualCameraStreamConfig implements Parcelable {
     // TODO(b/310857519): Check if we should increase the fps upper limit in future.
     static final int MAX_FPS_UPPER_LIMIT = 60;
-    // This is the minimum guaranteed upper bound of texture size supported by all devices.
-    // Keep this in sync with kMaxTextureSize from services/camera/virtualcamera/util/Util.cc
-    // TODO(b/310857519): Remove this once we add support for fetching the maximum texture size
-    // supported by the current device.
-    static final int DIMENSION_UPPER_LIMIT = 2048;
 
     private final int mWidth;
     private final int mHeight;
diff --git a/core/java/android/content/pm/ActivityInfo.java b/core/java/android/content/pm/ActivityInfo.java
index 535cebb..7ac9547 100644
--- a/core/java/android/content/pm/ActivityInfo.java
+++ b/core/java/android/content/pm/ActivityInfo.java
@@ -1548,6 +1548,26 @@
     public static final long INSETS_DECOUPLED_CONFIGURATION_ENFORCED = 151861875L;
 
     /**
+     * When enabled, the activity will receive configuration decoupled from system bar insets.
+     *
+     * <p>This will only apply if the activity is targeting SDK level 34 or earlier versions.
+     *
+     * <p>This will only in effect if the device is trying to provide a different value by default
+     * other than the legacy value, i.e., the
+     * {@code Flags.allowsScreenSizeDecoupledFromStatusBarAndCutout()} is set to true.
+     *
+     * <p>If the {@code Flags.insetsDecoupledConfiguration()} is also set to true, all apps
+     * targeting SDK level 35 or later, and apps with this override flag will receive the insets
+     * decoupled configuration.
+     *
+     * @hide
+     */
+    @ChangeId
+    @Disabled
+    @Overridable
+    public static final long OVERRIDE_ENABLE_INSETS_DECOUPLED_CONFIGURATION = 327313645L;
+
+    /**
      * Optional set of a certificates identifying apps that are allowed to embed this activity. From
      * the "knownActivityEmbeddingCerts" attribute.
      */
diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig
index 6d3a56d..c57a3a6 100644
--- a/core/java/android/content/pm/multiuser.aconfig
+++ b/core/java/android/content/pm/multiuser.aconfig
@@ -113,6 +113,26 @@
     is_fixed_read_only: true
 }
 
+flag {
+    name: "fix_avatar_picker_read_back_order"
+    namespace: "multiuser"
+    description: "Talkback focus doesn't move to the 'If you change your Google Account picture…' after swiping next to move the focus from 'Choose a picture'"
+    bug: "330835921"
+    metadata {
+    	purpose: PURPOSE_BUGFIX
+  }
+}
+
+flag {
+    name: "fix_avatar_picker_selected_read_back"
+    namespace: "multiuser"
+    description: "Talkback doesn't announce 'selected' after double tapping the button in the picture list in 'Choose a picture' page."
+    bug: "330840549"
+    metadata {
+    	purpose: PURPOSE_BUGFIX
+  }
+}
+
 # This flag guards the private space feature and all its implementations excluding the APIs. APIs are guarded by android.os.Flags.allow_private_profile.
 flag {
     name: "enable_private_space_features"
diff --git a/core/java/android/database/DatabaseUtils.java b/core/java/android/database/DatabaseUtils.java
index 7aa0349..3c4307c 100644
--- a/core/java/android/database/DatabaseUtils.java
+++ b/core/java/android/database/DatabaseUtils.java
@@ -1671,7 +1671,7 @@
             return null;
         }
         final int eos = Math.min(n+3, end);
-        return sql.substring(n, eos);
+        return sql.substring(n, eos).toUpperCase(Locale.ROOT);
     }
 
     /**
diff --git a/core/java/android/service/dreams/DreamOverlayService.java b/core/java/android/service/dreams/DreamOverlayService.java
index 9a02b74b..5da0cb4 100644
--- a/core/java/android/service/dreams/DreamOverlayService.java
+++ b/core/java/android/service/dreams/DreamOverlayService.java
@@ -79,6 +79,11 @@
             mService.endDream(this);
         }
 
+        @Override
+        public void comeToFront() {
+            mService.comeToFront(this);
+        }
+
         private void onExitRequested() {
             try {
                 mDreamOverlayCallback.onExitRequested();
@@ -130,6 +135,16 @@
         });
     }
 
+    private void comeToFront(OverlayClient client) {
+        mExecutor.execute(() -> {
+            if (mCurrentClient != client) {
+                return;
+            }
+
+            onComeToFront();
+        });
+    }
+
     private IDreamOverlay mDreamOverlay = new IDreamOverlay.Stub() {
         @Override
         public void getClient(IDreamOverlayClientCallback callback) {
@@ -190,6 +205,13 @@
     public void onWakeUp() {}
 
     /**
+     * This method is overridden by implementations to handle when the dream is coming to the front
+     * (after having lost focus to something on top of it).
+     * @hide
+     */
+    public void onComeToFront() {}
+
+    /**
      * This method is overridden by implementations to handle when the dream has ended. There may
      * be earlier signals leading up to this step, such as @{@link #onWakeUp(Runnable)}.
      *
diff --git a/core/java/android/service/dreams/DreamService.java b/core/java/android/service/dreams/DreamService.java
index 353828c..c26d83c 100644
--- a/core/java/android/service/dreams/DreamService.java
+++ b/core/java/android/service/dreams/DreamService.java
@@ -18,6 +18,7 @@
 
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
 import static android.service.dreams.Flags.dreamHandlesConfirmKeys;
+import static android.service.dreams.Flags.dreamTracksFocus;
 
 import android.annotation.FlaggedApi;
 import android.annotation.IdRes;
@@ -52,6 +53,7 @@
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.service.controls.flags.Flags;
+import android.service.dreams.utils.DreamAccessibility;
 import android.util.AttributeSet;
 import android.util.Log;
 import android.util.MathUtils;
@@ -273,6 +275,7 @@
     private boolean mDebug = false;
 
     private ComponentName mDreamComponent;
+    private DreamAccessibility mDreamAccessibility;
     private boolean mShouldShowComplications;
 
     private DreamServiceWrapper mDreamServiceWrapper;
@@ -455,6 +458,15 @@
     /** {@inheritDoc} */
     @Override
     public void onWindowFocusChanged(boolean hasFocus) {
+        if (!dreamTracksFocus()) {
+            return;
+        }
+
+        try {
+            mDreamManager.onDreamFocusChanged(hasFocus);
+        } catch (RemoteException ex) {
+            // system server died
+        }
     }
 
     /** {@inheritDoc} */
@@ -664,6 +676,7 @@
      */
     public void setInteractive(boolean interactive) {
         mInteractive = interactive;
+        updateAccessibilityMessage();
     }
 
     /**
@@ -1149,6 +1162,19 @@
         wakeUp(false);
     }
 
+    /**
+     * Tells the dream to come to the front (which in turn tells the overlay to come to the front).
+     */
+    private void comeToFront() {
+        mOverlayConnection.addConsumer(overlay -> {
+            try {
+                overlay.comeToFront();
+            } catch (RemoteException e) {
+                Log.e(mTag, "could not tell overlay to come to front:" + e);
+            }
+        });
+    }
+
     private void wakeUp(boolean fromSystem) {
         if (mDebug) {
             Slog.v(mTag, "wakeUp(): fromSystem=" + fromSystem + ", mWaking=" + mWaking
@@ -1430,7 +1456,7 @@
         // Hide all insets when the dream is showing
         mWindow.getDecorView().getWindowInsetsController().hide(WindowInsets.Type.systemBars());
         mWindow.setDecorFitsSystemWindows(false);
-
+        updateAccessibilityMessage();
         mWindow.getDecorView().addOnAttachStateChangeListener(
                 new View.OnAttachStateChangeListener() {
                     private Consumer<IDreamOverlayClient> mDreamStartOverlayConsumer;
@@ -1477,6 +1503,15 @@
                 });
     }
 
+    private void updateAccessibilityMessage() {
+        if (mWindow == null) return;
+        if (mDreamAccessibility == null) {
+            final View rootView = mWindow.getDecorView();
+            mDreamAccessibility = new DreamAccessibility(this, rootView);
+        }
+        mDreamAccessibility.updateAccessibilityConfiguration(isInteractive());
+    }
+
     private boolean getWindowFlagValue(int flag, boolean defaultValue) {
         return mWindow == null ? defaultValue : (mWindow.getAttributes().flags & flag) != 0;
     }
@@ -1596,6 +1631,15 @@
         public void wakeUp() {
             mHandler.post(() -> DreamService.this.wakeUp(true /*fromSystem*/));
         }
+
+        @Override
+        public void comeToFront() {
+            if (!dreamTracksFocus()) {
+                return;
+            }
+
+            mHandler.post(DreamService.this::comeToFront);
+        }
     }
 
     /** @hide */
diff --git a/core/java/android/service/dreams/IDreamManager.aidl b/core/java/android/service/dreams/IDreamManager.aidl
index e45384f..85f0368 100644
--- a/core/java/android/service/dreams/IDreamManager.aidl
+++ b/core/java/android/service/dreams/IDreamManager.aidl
@@ -48,4 +48,5 @@
     void setSystemDreamComponent(in ComponentName componentName);
     void registerDreamOverlayService(in ComponentName componentName);
     void startDreamActivity(in Intent intent);
+    void onDreamFocusChanged(in boolean hasFocus);
 }
diff --git a/core/java/android/service/dreams/IDreamOverlayClient.aidl b/core/java/android/service/dreams/IDreamOverlayClient.aidl
index 78b7280..5054d4d 100644
--- a/core/java/android/service/dreams/IDreamOverlayClient.aidl
+++ b/core/java/android/service/dreams/IDreamOverlayClient.aidl
@@ -42,4 +42,7 @@
 
     /** Called when the dream has ended. */
     void endDream();
+
+    /** Called when the dream is coming to the front. */
+    void comeToFront();
 }
diff --git a/core/java/android/service/dreams/IDreamService.aidl b/core/java/android/service/dreams/IDreamService.aidl
index 8b5d875..2e2651b 100644
--- a/core/java/android/service/dreams/IDreamService.aidl
+++ b/core/java/android/service/dreams/IDreamService.aidl
@@ -25,4 +25,5 @@
     void attach(IBinder windowToken, boolean canDoze, boolean isPreviewMode, IRemoteCallback started);
     void detach();
     void wakeUp();
+    void comeToFront();
 }
diff --git a/core/java/android/service/dreams/flags.aconfig b/core/java/android/service/dreams/flags.aconfig
index 88f1090..0a458bc 100644
--- a/core/java/android/service/dreams/flags.aconfig
+++ b/core/java/android/service/dreams/flags.aconfig
@@ -19,3 +19,10 @@
     purpose: PURPOSE_BUGFIX
   }
 }
+
+flag {
+  name: "dream_tracks_focus"
+  namespace: "communal"
+  description: "This flag enables the ability for dreams to track whether or not they have focus"
+  bug: "331798001"
+}
diff --git a/core/java/android/service/dreams/utils/DreamAccessibility.java b/core/java/android/service/dreams/utils/DreamAccessibility.java
new file mode 100644
index 0000000..c38f41b
--- /dev/null
+++ b/core/java/android/service/dreams/utils/DreamAccessibility.java
@@ -0,0 +1,88 @@
+/*
+ * 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 android.service.dreams.utils;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.view.View;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+import com.android.internal.R;
+
+/**
+ * {@link DreamAccessibility} allows customization of accessibility
+ * actions for the root view of the dream overlay.
+ * @hide
+ */
+public class DreamAccessibility {
+    private final Context mContext;
+    private final View mView;
+    private final View.AccessibilityDelegate mAccessibilityDelegate;
+
+    public DreamAccessibility(@NonNull Context context, @NonNull View view) {
+        mContext = context;
+        mView = view;
+        mAccessibilityDelegate = createNewAccessibilityDelegate(mContext);
+    }
+
+    /**
+     * @param interactive
+     * Removes and add accessibility configuration depending if the dream is interactive or not
+     */
+    public void updateAccessibilityConfiguration(Boolean interactive) {
+        if (!interactive) {
+            addAccessibilityConfiguration();
+        } else {
+            removeCustomAccessibilityAction();
+        }
+    }
+
+    /**
+     * Configures the accessibility actions for the given root view.
+     */
+    private void addAccessibilityConfiguration() {
+        mView.setAccessibilityDelegate(mAccessibilityDelegate);
+    }
+
+    /**
+     * Removes Configured the accessibility actions for the given root view.
+     */
+    private void removeCustomAccessibilityAction() {
+        if (mView.getAccessibilityDelegate() == mAccessibilityDelegate) {
+            mView.setAccessibilityDelegate(null);
+        }
+    }
+
+    private View.AccessibilityDelegate createNewAccessibilityDelegate(Context context) {
+        return new View.AccessibilityDelegate() {
+            @Override
+            public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
+                super.onInitializeAccessibilityNodeInfo(host, info);
+                for (AccessibilityNodeInfo.AccessibilityAction action : info.getActionList()) {
+                    if (action.getId() == AccessibilityNodeInfo.ACTION_CLICK) {
+                        info.removeAction(action);
+                        break;
+                    }
+                }
+                info.addAction(new AccessibilityNodeInfo.AccessibilityAction(
+                        AccessibilityNodeInfo.ACTION_CLICK,
+                        context.getResources().getString(R.string.dream_accessibility_action_click)
+                ));
+            }
+        };
+    }
+}
diff --git a/core/java/android/service/notification/ZenPolicy.java b/core/java/android/service/notification/ZenPolicy.java
index 786d768..1d7091c 100644
--- a/core/java/android/service/notification/ZenPolicy.java
+++ b/core/java/android/service/notification/ZenPolicy.java
@@ -349,23 +349,29 @@
     /**
      * Indicates no explicit setting for which channels may bypass DND when this policy is active.
      * Defaults to {@link #CHANNEL_POLICY_PRIORITY}.
+     *
+     * @hide
      */
     @FlaggedApi(Flags.FLAG_MODES_API)
-    private static final int CHANNEL_POLICY_UNSET = 0;
+    public static final int CHANNEL_POLICY_UNSET = 0;
 
     /**
      * Indicates that channels marked as {@link NotificationChannel#canBypassDnd()} can bypass DND
      * when this policy is active.
+     *
+     * @hide
      */
     @FlaggedApi(Flags.FLAG_MODES_API)
-    private static final int CHANNEL_POLICY_PRIORITY = 1;
+    public static final int CHANNEL_POLICY_PRIORITY = 1;
 
     /**
      * Indicates that no channels can bypass DND when this policy is active, even those marked as
      * {@link NotificationChannel#canBypassDnd()}.
+     *
+     * @hide
      */
     @FlaggedApi(Flags.FLAG_MODES_API)
-    private static final int CHANNEL_POLICY_NONE = 2;
+    public static final int CHANNEL_POLICY_NONE = 2;
 
     /** @hide */
     public ZenPolicy() {
@@ -564,6 +570,13 @@
     }
 
     /**
+     * @hide
+     */
+    public @ChannelType int getAllowedChannels() {
+        return mAllowChannels;
+    }
+
+    /**
      * Whether this policy allows {@link NotificationChannel channels} marked as
      * {@link NotificationChannel#canBypassDnd()} to bypass DND. If {@link #STATE_ALLOW}, these
      * channels may bypass; if {@link #STATE_DISALLOW}, then even notifications from channels
@@ -999,6 +1012,12 @@
             mZenPolicy.mAllowChannels = allow ? CHANNEL_POLICY_PRIORITY : CHANNEL_POLICY_NONE;
             return this;
         }
+
+        /** @hide */
+        public @NonNull Builder allowChannels(@ChannelType int channelType) {
+            mZenPolicy.mAllowChannels = channelType;
+            return this;
+        }
     }
 
     @Override
diff --git a/core/java/android/telephony/PhoneStateListener.java b/core/java/android/telephony/PhoneStateListener.java
index fb57921..4281da1 100644
--- a/core/java/android/telephony/PhoneStateListener.java
+++ b/core/java/android/telephony/PhoneStateListener.java
@@ -1685,6 +1685,10 @@
         public final void onSimultaneousCallingStateChanged(int[] subIds) {
             // not supported on the deprecated interface - Use TelephonyCallback instead
         }
+
+        public final void onCarrierRoamingNtnModeChanged(boolean active) {
+            // not supported on the deprecated interface - Use TelephonyCallback instead
+        }
     }
 
     private void log(String s) {
diff --git a/core/java/android/telephony/TelephonyCallback.java b/core/java/android/telephony/TelephonyCallback.java
index dc6a035..b8b84d9 100644
--- a/core/java/android/telephony/TelephonyCallback.java
+++ b/core/java/android/telephony/TelephonyCallback.java
@@ -644,6 +644,15 @@
     public static final int EVENT_SIMULTANEOUS_CELLULAR_CALLING_SUBSCRIPTIONS_CHANGED = 41;
 
     /**
+     * Event for listening to changes in carrier roaming non-terrestrial network mode.
+     *
+     * @see CarrierRoamingNtnModeListener
+     *
+     * @hide
+     */
+    public static final int EVENT_CARRIER_ROAMING_NTN_MODE_CHANGED = 42;
+
+    /**
      * @hide
      */
     @IntDef(prefix = {"EVENT_"}, value = {
@@ -687,7 +696,8 @@
             EVENT_TRIGGER_NOTIFY_ANBR,
             EVENT_MEDIA_QUALITY_STATUS_CHANGED,
             EVENT_EMERGENCY_CALLBACK_MODE_CHANGED,
-            EVENT_SIMULTANEOUS_CELLULAR_CALLING_SUBSCRIPTIONS_CHANGED
+            EVENT_SIMULTANEOUS_CELLULAR_CALLING_SUBSCRIPTIONS_CHANGED,
+            EVENT_CARRIER_ROAMING_NTN_MODE_CHANGED
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface TelephonyEvent {
@@ -1686,6 +1696,24 @@
     }
 
     /**
+     * Interface for carrier roaming non-terrestrial network listener.
+     *
+     * @hide
+     */
+    public interface CarrierRoamingNtnModeListener {
+        /**
+         * Callback invoked when carrier roaming non-terrestrial network mode changes.
+         *
+         * @param active {@code true} If the device is connected to carrier roaming
+         *                           non-terrestrial network or was connected within the
+         *                           {CarrierConfigManager
+         *                           #KEY_SATELLITE_CONNECTION_HYSTERESIS_SEC_INT} duration,
+         *                           {code false} otherwise.
+         */
+        void onCarrierRoamingNtnModeChanged(boolean active);
+    }
+
+    /**
      * The callback methods need to be called on the handler thread where
      * this object was created.  If the binder did that for us it'd be nice.
      * <p>
@@ -2086,5 +2114,16 @@
             Binder.withCleanCallingIdentity(
                     () -> mExecutor.execute(() -> listener.onCallBackModeStopped(type, reason)));
         }
+
+        public void onCarrierRoamingNtnModeChanged(boolean active) {
+            if (!Flags.carrierEnabledSatelliteFlag()) return;
+
+            CarrierRoamingNtnModeListener listener =
+                    (CarrierRoamingNtnModeListener) mTelephonyCallbackWeakRef.get();
+            if (listener == null) return;
+
+            Binder.withCleanCallingIdentity(
+                    () -> mExecutor.execute(() -> listener.onCarrierRoamingNtnModeChanged(active)));
+        }
     }
 }
diff --git a/core/java/android/telephony/TelephonyRegistryManager.java b/core/java/android/telephony/TelephonyRegistryManager.java
index d39c4ce..6160fdb 100644
--- a/core/java/android/telephony/TelephonyRegistryManager.java
+++ b/core/java/android/telephony/TelephonyRegistryManager.java
@@ -1073,6 +1073,24 @@
     }
 
     /**
+     * Notify external listeners that carrier roaming non-terrestrial network mode changed.
+     * @param subId subscription ID.
+     * @param active {@code true} If the device is connected to carrier roaming
+     *                           non-terrestrial network or was connected within the
+     *                           {CarrierConfigManager#KEY_SATELLITE_CONNECTION_HYSTERESIS_SEC_INT}
+     *                           duration, {code false} otherwise.
+     * @hide
+     */
+    public void notifyCarrierRoamingNtnModeChanged(int subId, boolean active) {
+        try {
+            sRegistry.notifyCarrierRoamingNtnModeChanged(subId, active);
+        } catch (RemoteException ex) {
+            // system server crash
+            throw ex.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * Processes potential event changes from the provided {@link TelephonyCallback}.
      *
      * @param telephonyCallback callback for monitoring callback changes to the telephony state.
@@ -1224,6 +1242,10 @@
             eventList.add(
                     TelephonyCallback.EVENT_SIMULTANEOUS_CELLULAR_CALLING_SUBSCRIPTIONS_CHANGED);
         }
+
+        if (telephonyCallback instanceof TelephonyCallback.CarrierRoamingNtnModeListener) {
+            eventList.add(TelephonyCallback.EVENT_CARRIER_ROAMING_NTN_MODE_CHANGED);
+        }
         return eventList;
     }
 
diff --git a/core/java/android/text/MeasuredParagraph.java b/core/java/android/text/MeasuredParagraph.java
index 38aeb37..161a79b 100644
--- a/core/java/android/text/MeasuredParagraph.java
+++ b/core/java/android/text/MeasuredParagraph.java
@@ -30,6 +30,8 @@
 import android.graphics.Rect;
 import android.graphics.text.LineBreakConfig;
 import android.graphics.text.MeasuredText;
+import android.icu.lang.UCharacter;
+import android.icu.lang.UCharacterDirection;
 import android.icu.text.Bidi;
 import android.text.AutoGrowArray.ByteArray;
 import android.text.AutoGrowArray.FloatArray;
@@ -711,7 +713,12 @@
                 // check the paragraph count here and replace the CR letters and re-calculate
                 // BiDi again.
                 for (int i = 0; i < mTextLength; ++i) {
-                    if (mCopiedBuffer[i] == '\r') {
+                    if (Character.isSurrogate(mCopiedBuffer[i])) {
+                        // All block separators are in BMP.
+                        continue;
+                    }
+                    if (UCharacter.getDirection(mCopiedBuffer[i])
+                            == UCharacterDirection.BLOCK_SEPARATOR) {
                         mCopiedBuffer[i] = OBJECT_REPLACEMENT_CHARACTER;
                     }
                 }
diff --git a/core/java/android/text/TextLine.java b/core/java/android/text/TextLine.java
index bde9c77..3015791 100644
--- a/core/java/android/text/TextLine.java
+++ b/core/java/android/text/TextLine.java
@@ -477,7 +477,12 @@
             }
             drawBounds.setEmpty();
             float w = measure(mLen, false, fmi, drawBounds, lineInfo);
-            float boundsWidth = drawBounds.width();
+            float boundsWidth;
+            if (w >= 0) {
+                boundsWidth = Math.max(drawBounds.right, w) - Math.min(0, drawBounds.left);
+            } else {
+                boundsWidth = Math.max(drawBounds.right, 0) - Math.min(w, drawBounds.left);
+            }
             if (Math.abs(w) > boundsWidth) {
                 return w;
             } else {
diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl
index 51d7caa..65d9b3a 100644
--- a/core/java/android/view/IWindowManager.aidl
+++ b/core/java/android/view/IWindowManager.aidl
@@ -291,6 +291,15 @@
     int getDefaultDisplayRotation();
 
     /**
+     * Retrieve the display user rotation.
+     * @param displayId Id of the display
+     * @return Rotation one of {@link android.view.Surface#ROTATION_0},
+     *        {@link android.view.Surface#ROTATION_90}, {@link android.view.Surface#ROTATION_180},
+     *        {@link android.view.Surface#ROTATION_270} or -1 if display is not found.
+     */
+    int getDisplayUserRotation(int displayId);
+
+    /**
      * Watch the rotation of the specified screen.  Returns the current rotation,
      * calls back when it changes.
      */
diff --git a/core/java/android/view/ViewStructure.java b/core/java/android/view/ViewStructure.java
index 6c852c3..86e5bea 100644
--- a/core/java/android/view/ViewStructure.java
+++ b/core/java/android/view/ViewStructure.java
@@ -76,6 +76,21 @@
             "android.view.ViewStructure.extra.FIRST_ACTIVE_POSITION";
 
     /**
+     * Key used for writing the type of the view that generated the virtual structure of its
+     * children.
+     *
+     * For example, if the virtual structure is generated by a webview, the value would be
+     * "WebView". If the virtual structure is generated by a compose view, then the value would be
+     * "ComposeView". The value is of type String.
+     *
+     * This value is added to mainly help with debugging purpose.
+     *
+     * @hide
+     */
+    public static final String EXTRA_VIRTUAL_STRUCTURE_TYPE =
+            "android.view.ViewStructure.extra.VIRTUAL_STRUCTURE_TYPE";
+
+    /**
      * Set the identifier for this view.
      *
      * @param id The view's identifier, as per {@link View#getId View.getId()}.
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index fbb5116..fb1c331 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -11254,8 +11254,10 @@
                 width = des;
             } else {
                 if (mUseBoundsForWidth) {
-                    width = Math.max(boring.width,
-                            (int) Math.ceil(boring.getDrawingBoundingBox().width()));
+                    RectF bbox = boring.getDrawingBoundingBox();
+                    float rightMax = Math.max(bbox.right, boring.width);
+                    float leftMin = Math.min(bbox.left, 0);
+                    width = Math.max(boring.width, (int) Math.ceil(rightMax - leftMin));
                 } else {
                     width = boring.width;
                 }
diff --git a/core/java/com/android/internal/os/flags.aconfig b/core/java/com/android/internal/os/flags.aconfig
index 4c2bbd4..c8d6810 100644
--- a/core/java/com/android/internal/os/flags.aconfig
+++ b/core/java/com/android/internal/os/flags.aconfig
@@ -1,4 +1,5 @@
 package: "com.android.internal.os"
+container: "system"
 
 flag {
     name: "enable_apache_http_legacy_preload"
@@ -7,4 +8,4 @@
     # Fixed read-only is required as the flag is read during zygote init.
     is_fixed_read_only: true
     bug: "241474956"
-}
\ No newline at end of file
+}
diff --git a/core/java/com/android/internal/telephony/IPhoneStateListener.aidl b/core/java/com/android/internal/telephony/IPhoneStateListener.aidl
index 969f95d..792c223 100644
--- a/core/java/com/android/internal/telephony/IPhoneStateListener.aidl
+++ b/core/java/com/android/internal/telephony/IPhoneStateListener.aidl
@@ -81,4 +81,5 @@
     void onCallBackModeStarted(int type);
     void onCallBackModeStopped(int type, int reason);
     void onSimultaneousCallingStateChanged(in int[] subIds);
+    void onCarrierRoamingNtnModeChanged(in boolean active);
 }
diff --git a/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl b/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl
index 0203ea4..04332cd 100644
--- a/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl
+++ b/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl
@@ -120,4 +120,5 @@
 
     void notifyCallbackModeStarted(int phoneId, int subId, int type);
     void notifyCallbackModeStopped(int phoneId, int subId, int type, int reason);
+    void notifyCarrierRoamingNtnModeChanged(int subId, in boolean active);
 }
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 60289c1..e96240d 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -1018,6 +1018,9 @@
     <!-- The title to use when a dream is opened in preview mode. [CHAR LIMIT=NONE] -->
     <string name="dream_preview_title">Preview, <xliff:g id="dream_name" example="Clock">%1$s</xliff:g></string>
 
+    <!-- The title to use when a dream is open in accessibility mode to let users know to double tap to dismiss the dream  [CHAR LIMIT=32] -->
+    <string name="dream_accessibility_action_click">dismiss</string>
+
     <!--  Permissions -->
 
     <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 624026c..0bf6347 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -4471,6 +4471,7 @@
   <java-symbol type="string" name="capability_title_canTakeScreenshot" />
 
   <java-symbol type="string" name="dream_preview_title" />
+  <java-symbol type="string" name="dream_accessibility_action_click" />
 
   <java-symbol type="string" name="config_servicesExtensionPackage" />
 
diff --git a/core/tests/coretests/src/android/database/DatabaseUtilsTest.java b/core/tests/coretests/src/android/database/DatabaseUtilsTest.java
index c9cb2cc..e25fdf9 100644
--- a/core/tests/coretests/src/android/database/DatabaseUtilsTest.java
+++ b/core/tests/coretests/src/android/database/DatabaseUtilsTest.java
@@ -90,6 +90,12 @@
         assertEquals(ddl, getSqlStatementType("ALTER TABLE t1 ADD COLUMN j int"));
         assertEquals(ddl, getSqlStatementType("CREATE TABLE t1 (i int)"));
 
+        // Verify that the answers are case-insensitive
+        assertEquals(sel, getSqlStatementType("select"));
+        assertEquals(sel, getSqlStatementType("sElect"));
+        assertEquals(sel, getSqlStatementType("sELECT"));
+        assertEquals(sel, getSqlStatementType("seLECT"));
+
         // Short statements, leading comments, and WITH are decoded to "other" in the public API.
         final int othr = STATEMENT_OTHER;
         assertEquals(othr, getSqlStatementType("SE"));
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java
index a67821b..0297901 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java
@@ -288,13 +288,16 @@
 
     /** Sets the position of the dot and badge, animating them out and back in if requested. */
     void animateDotBadgePositions(boolean onLeft) {
-        mOnLeft = onLeft;
-
-        if (onLeft != getDotOnLeft() && shouldDrawDot()) {
-            animateDotScale(0f /* showDot */, () -> {
-                invalidate();
-                animateDotScale(1.0f, null /* after */);
-            });
+        if (onLeft != getDotOnLeft()) {
+            if (shouldDrawDot()) {
+                animateDotScale(0f /* showDot */, () -> {
+                    mOnLeft = onLeft;
+                    invalidate();
+                    animateDotScale(1.0f, null /* after */);
+                });
+            } else {
+                mOnLeft = onLeft;
+            }
         }
         // TODO animate badge
         showBadge();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
index 74f087b..0b3c2ba3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
@@ -446,6 +446,8 @@
             mManageButton.setVisibility(GONE);
         } else {
             mTaskView = bubbleTaskView.getTaskView();
+            // reset the insets that might left after TaskView is shown in BubbleBarExpandedView
+            mTaskView.setCaptionInsets(null);
             bubbleTaskView.setDelegateListener(mTaskViewListener);
 
             // set a fixed width so it is not recalculated as part of a rotation. the width will be
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
index 57cf992..e885262 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
@@ -597,6 +597,17 @@
             return;
         }
 
+        if (mPipTransitionState.isEnteringPip()
+                && !mPipTransitionState.getInSwipePipToHomeTransition()) {
+            // If we are still entering PiP with Shell playing enter animation, jump-cut to
+            // the end of the enter animation and reschedule exitPip to run after enter-PiP
+            // has finished its transition and allowed the client to draw in PiP mode.
+            mPipTransitionController.end(() -> {
+                exitPip(animationDurationMs, requestEnterSplit);
+            });
+            return;
+        }
+
         ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
                 "exitPip: %s, state=%s", mTaskInfo.topActivity, mPipTransitionState);
         final WindowContainerTransaction wct = new WindowContainerTransaction();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
index 10b7619..fdde3ee 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
@@ -43,7 +43,6 @@
 import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP_TO_SPLIT;
 import static com.android.wm.shell.transition.Transitions.TRANSIT_REMOVE_PIP;
 
-import android.animation.Animator;
 import android.annotation.IntDef;
 import android.app.ActivityManager;
 import android.app.TaskInfo;
@@ -348,9 +347,16 @@
 
     @Override
     public void end() {
-        Animator animator = mPipAnimationController.getCurrentAnimator();
-        if (animator != null && animator.isRunning()) {
-            animator.end();
+        end(null);
+    }
+
+    @Override
+    public void end(@Nullable Runnable onTransitionEnd) {
+        if (mPipAnimationController.isAnimating()) {
+            mPipAnimationController.getCurrentAnimator().end();
+        }
+        if (onTransitionEnd != null) {
+            onTransitionEnd.run();
         }
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java
index 32442f7..4f71a02 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java
@@ -305,6 +305,14 @@
     public void end() {
     }
 
+    /**
+     * End the currently-playing PiP animation.
+     *
+     * @param onTransitionEnd callback to run upon finishing the playing transition.
+     */
+    public void end(@Nullable Runnable onTransitionEnd) {
+    }
+
     /** Starts the {@link android.window.SystemPerformanceHinter.HighPerfSession}. */
     public void startHighPerfSession() {}
 
diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/CloseAllAppWithAppHeaderExitLandscape.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/CloseAllAppWithAppHeaderExitLandscape.kt
new file mode 100644
index 0000000..5563bb9
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/CloseAllAppWithAppHeaderExitLandscape.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.service.desktopmode.flicker
+
+import android.tools.Rotation
+import android.tools.flicker.FlickerConfig
+import android.tools.flicker.annotation.ExpectedScenarios
+import android.tools.flicker.annotation.FlickerConfigProvider
+import android.tools.flicker.config.FlickerConfig
+import android.tools.flicker.config.FlickerServiceConfig
+import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner
+import com.android.wm.shell.flicker.service.desktopmode.flicker.DesktopModeFlickerScenarios.Companion.CLOSE_APP
+import com.android.wm.shell.flicker.service.desktopmode.flicker.DesktopModeFlickerScenarios.Companion.CLOSE_LAST_APP
+import com.android.wm.shell.flicker.service.desktopmode.scenarios.CloseAllAppsWithAppHeaderExit
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(FlickerServiceJUnit4ClassRunner::class)
+class CloseAllAppWithAppHeaderExitLandscape : CloseAllAppsWithAppHeaderExit(Rotation.ROTATION_90) {
+    @ExpectedScenarios(["CLOSE_APP", "CLOSE_LAST_APP"])
+    @Test
+    override fun closeAllAppsInDesktop() = super.closeAllAppsInDesktop()
+
+    companion object {
+        @JvmStatic
+        @FlickerConfigProvider
+        fun flickerConfigProvider(): FlickerConfig =
+            FlickerConfig()
+                .use(FlickerServiceConfig.DEFAULT)
+                .use(CLOSE_APP)
+                .use(CLOSE_LAST_APP)
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/CloseAllAppWithAppHeaderExitPortrait.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/CloseAllAppWithAppHeaderExitPortrait.kt
new file mode 100644
index 0000000..3d16d2219
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/CloseAllAppWithAppHeaderExitPortrait.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.service.desktopmode.flicker
+
+import android.tools.Rotation
+import android.tools.flicker.FlickerConfig
+import android.tools.flicker.annotation.ExpectedScenarios
+import android.tools.flicker.annotation.FlickerConfigProvider
+import android.tools.flicker.config.FlickerConfig
+import android.tools.flicker.config.FlickerServiceConfig
+import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner
+import com.android.wm.shell.flicker.service.desktopmode.flicker.DesktopModeFlickerScenarios.Companion.CLOSE_APP
+import com.android.wm.shell.flicker.service.desktopmode.flicker.DesktopModeFlickerScenarios.Companion.CLOSE_LAST_APP
+import com.android.wm.shell.flicker.service.desktopmode.scenarios.CloseAllAppsWithAppHeaderExit
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(FlickerServiceJUnit4ClassRunner::class)
+class CloseAllAppWithAppHeaderExitPortrait : CloseAllAppsWithAppHeaderExit(Rotation.ROTATION_0) {
+    @ExpectedScenarios(["CLOSE_APP", "CLOSE_LAST_APP"])
+    @Test
+    override fun closeAllAppsInDesktop() = super.closeAllAppsInDesktop()
+
+    companion object {
+        @JvmStatic
+        @FlickerConfigProvider
+        fun flickerConfigProvider(): FlickerConfig =
+            FlickerConfig()
+                .use(FlickerServiceConfig.DEFAULT)
+                .use(CLOSE_APP)
+                .use(CLOSE_LAST_APP)
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/DesktopModeFlickerScenarios.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/DesktopModeFlickerScenarios.kt
new file mode 100644
index 0000000..75dfeba
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/DesktopModeFlickerScenarios.kt
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.service.desktopmode.flicker
+
+import android.tools.flicker.AssertionInvocationGroup
+import android.tools.flicker.assertors.assertions.AppLayerIsInvisibleAtEnd
+import android.tools.flicker.assertors.assertions.AppLayerIsVisibleAlways
+import android.tools.flicker.assertors.assertions.AppLayerIsVisibleAtStart
+import android.tools.flicker.assertors.assertions.AppWindowHasDesktopModeInitialBoundsAtTheEnd
+import android.tools.flicker.assertors.assertions.AppWindowOnTopAtEnd
+import android.tools.flicker.assertors.assertions.AppWindowOnTopAtStart
+import android.tools.flicker.assertors.assertions.LauncherWindowMovesToTop
+import android.tools.flicker.config.AssertionTemplates
+import android.tools.flicker.config.FlickerConfigEntry
+import android.tools.flicker.config.ScenarioId
+import android.tools.flicker.config.desktopmode.Components
+import android.tools.flicker.extractors.ITransitionMatcher
+import android.tools.flicker.extractors.ShellTransitionScenarioExtractor
+import android.tools.traces.wm.Transition
+import android.tools.traces.wm.TransitionType
+
+class DesktopModeFlickerScenarios {
+    companion object {
+        val END_DRAG_TO_DESKTOP =
+            FlickerConfigEntry(
+                scenarioId = ScenarioId("END_DRAG_TO_DESKTOP"),
+                extractor =
+                    ShellTransitionScenarioExtractor(
+                        transitionMatcher =
+                            object : ITransitionMatcher {
+                                override fun findAll(
+                                    transitions: Collection<Transition>
+                                ): Collection<Transition> {
+                                    return transitions.filter {
+                                        it.type == TransitionType.DESKTOP_MODE_END_DRAG_TO_DESKTOP
+                                    }
+                                }
+                            }
+                    ),
+                assertions =
+                    AssertionTemplates.COMMON_ASSERTIONS +
+                        listOf(
+                                AppLayerIsVisibleAlways(Components.DESKTOP_MODE_APP),
+                                AppWindowOnTopAtEnd(Components.DESKTOP_MODE_APP),
+                                AppWindowHasDesktopModeInitialBoundsAtTheEnd(
+                                    Components.DESKTOP_MODE_APP
+                                )
+                            )
+                            .associateBy({ it }, { AssertionInvocationGroup.BLOCKING }),
+            )
+
+        val CLOSE_APP =
+            FlickerConfigEntry(
+                scenarioId = ScenarioId("CLOSE_APP"),
+                extractor =
+                    ShellTransitionScenarioExtractor(
+                        transitionMatcher =
+                            object : ITransitionMatcher {
+                                override fun findAll(
+                                    transitions: Collection<Transition>
+                                ): Collection<Transition> {
+                                    return transitions.filter { it.type == TransitionType.CLOSE }
+                                }
+                            }
+                    ),
+                assertions =
+                    AssertionTemplates.COMMON_ASSERTIONS +
+                        listOf(
+                                AppWindowOnTopAtStart(Components.DESKTOP_MODE_APP),
+                                AppLayerIsVisibleAtStart(Components.DESKTOP_MODE_APP),
+                                AppLayerIsInvisibleAtEnd(Components.DESKTOP_MODE_APP),
+                            )
+                            .associateBy({ it }, { AssertionInvocationGroup.BLOCKING }),
+            )
+
+        val CLOSE_LAST_APP =
+            FlickerConfigEntry(
+                scenarioId = ScenarioId("CLOSE_LAST_APP"),
+                extractor =
+                    ShellTransitionScenarioExtractor(
+                        transitionMatcher =
+                            object : ITransitionMatcher {
+                                override fun findAll(
+                                    transitions: Collection<Transition>
+                                ): Collection<Transition> {
+                                    val lastTransition =
+                                        transitions.findLast { it.type == TransitionType.CLOSE }
+                                    return if (lastTransition != null) listOf(lastTransition)
+                                    else emptyList()
+                                }
+                            }
+                    ),
+                assertions =
+                    AssertionTemplates.COMMON_ASSERTIONS +
+                        listOf(
+                                AppWindowOnTopAtStart(Components.DESKTOP_MODE_APP),
+                                AppLayerIsVisibleAtStart(Components.DESKTOP_MODE_APP),
+                                AppLayerIsInvisibleAtEnd(Components.DESKTOP_MODE_APP),
+                                LauncherWindowMovesToTop()
+                            )
+                            .associateBy({ it }, { AssertionInvocationGroup.BLOCKING }),
+            )
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/EnterDesktopWithDragLandscape.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/EnterDesktopWithDragLandscape.kt
index 4c781d3..9dfafe9 100644
--- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/EnterDesktopWithDragLandscape.kt
+++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/EnterDesktopWithDragLandscape.kt
@@ -17,58 +17,28 @@
 package com.android.wm.shell.flicker.service.desktopmode.flicker
 
 import android.tools.Rotation
-import android.tools.flicker.AssertionInvocationGroup
 import android.tools.flicker.FlickerConfig
 import android.tools.flicker.annotation.ExpectedScenarios
 import android.tools.flicker.annotation.FlickerConfigProvider
-import android.tools.flicker.assertors.assertions.AppLayerIsVisibleAlways
-import android.tools.flicker.assertors.assertions.AppWindowHasDesktopModeInitialBoundsAtTheEnd
-import android.tools.flicker.assertors.assertions.AppWindowOnTopAtEnd
-import android.tools.flicker.config.AssertionTemplates
 import android.tools.flicker.config.FlickerConfig
-import android.tools.flicker.config.FlickerConfigEntry
 import android.tools.flicker.config.FlickerServiceConfig
-import android.tools.flicker.config.ScenarioId
-import android.tools.flicker.config.desktopmode.Components
-import android.tools.flicker.extractors.ITransitionMatcher
-import android.tools.flicker.extractors.ShellTransitionScenarioExtractor
 import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner
-import android.tools.traces.wm.Transition
-import android.tools.traces.wm.TransitionType
+import com.android.wm.shell.flicker.service.desktopmode.flicker.DesktopModeFlickerScenarios.Companion.END_DRAG_TO_DESKTOP
 import com.android.wm.shell.flicker.service.desktopmode.scenarios.EnterDesktopWithDrag
 import org.junit.Test
 import org.junit.runner.RunWith
 
 @RunWith(FlickerServiceJUnit4ClassRunner::class)
 class EnterDesktopWithDragLandscape : EnterDesktopWithDrag(Rotation.ROTATION_90) {
-    @ExpectedScenarios(["END_DRAG_TO_DESKTOP"]) @Test override fun enterDesktopWithDrag() =
-        super.enterDesktopWithDrag()
+    @ExpectedScenarios(["END_DRAG_TO_DESKTOP"])
+    @Test
+    override fun enterDesktopWithDrag() = super.enterDesktopWithDrag()
 
     companion object {
-        private val END_DRAG_TO_DESKTOP = FlickerConfigEntry(
-            scenarioId = ScenarioId("END_DRAG_TO_DESKTOP"),
-            extractor = ShellTransitionScenarioExtractor(
-                transitionMatcher = object : ITransitionMatcher {
-                    override fun findAll(
-                        transitions: Collection<Transition>
-                    ): Collection<Transition> {
-                        return transitions.filter {
-                            it.type == TransitionType.DESKTOP_MODE_END_DRAG_TO_DESKTOP}
-                    }
-                }),
-            assertions = AssertionTemplates.COMMON_ASSERTIONS +
-                    listOf(
-                        AppLayerIsVisibleAlways(Components.DESKTOP_MODE_APP),
-                        AppWindowOnTopAtEnd(Components.DESKTOP_MODE_APP),
-                        AppWindowHasDesktopModeInitialBoundsAtTheEnd(Components.DESKTOP_MODE_APP)
-                    ).associateBy({ it }, { AssertionInvocationGroup.BLOCKING }),
-        )
 
         @JvmStatic
         @FlickerConfigProvider
         fun flickerConfigProvider(): FlickerConfig =
-            FlickerConfig()
-                    .use(FlickerServiceConfig.DEFAULT)
-                    .use(END_DRAG_TO_DESKTOP)
+            FlickerConfig().use(FlickerServiceConfig.DEFAULT).use(END_DRAG_TO_DESKTOP)
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/EnterDesktopWithDragPortrait.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/EnterDesktopWithDragPortrait.kt
index d99d875..1c7d623 100644
--- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/EnterDesktopWithDragPortrait.kt
+++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/EnterDesktopWithDragPortrait.kt
@@ -17,58 +17,27 @@
 package com.android.wm.shell.flicker.service.desktopmode.flicker
 
 import android.tools.Rotation
-import android.tools.flicker.AssertionInvocationGroup
 import android.tools.flicker.FlickerConfig
 import android.tools.flicker.annotation.ExpectedScenarios
 import android.tools.flicker.annotation.FlickerConfigProvider
-import android.tools.flicker.assertors.assertions.AppLayerIsVisibleAlways
-import android.tools.flicker.assertors.assertions.AppWindowHasDesktopModeInitialBoundsAtTheEnd
-import android.tools.flicker.assertors.assertions.AppWindowOnTopAtEnd
-import android.tools.flicker.config.AssertionTemplates
 import android.tools.flicker.config.FlickerConfig
-import android.tools.flicker.config.FlickerConfigEntry
 import android.tools.flicker.config.FlickerServiceConfig
-import android.tools.flicker.config.ScenarioId
-import android.tools.flicker.config.desktopmode.Components
-import android.tools.flicker.extractors.ITransitionMatcher
-import android.tools.flicker.extractors.ShellTransitionScenarioExtractor
 import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner
-import android.tools.traces.wm.Transition
-import android.tools.traces.wm.TransitionType
+import com.android.wm.shell.flicker.service.desktopmode.flicker.DesktopModeFlickerScenarios.Companion.END_DRAG_TO_DESKTOP
 import com.android.wm.shell.flicker.service.desktopmode.scenarios.EnterDesktopWithDrag
 import org.junit.Test
 import org.junit.runner.RunWith
 
 @RunWith(FlickerServiceJUnit4ClassRunner::class)
 class EnterDesktopWithDragPortrait : EnterDesktopWithDrag(Rotation.ROTATION_0) {
-    @ExpectedScenarios(["END_DRAG_TO_DESKTOP"]) @Test override fun enterDesktopWithDrag() =
-        super.enterDesktopWithDrag()
+    @ExpectedScenarios(["END_DRAG_TO_DESKTOP"])
+    @Test
+    override fun enterDesktopWithDrag() = super.enterDesktopWithDrag()
 
     companion object {
-        private val END_DRAG_TO_DESKTOP = FlickerConfigEntry(
-            scenarioId = ScenarioId("END_DRAG_TO_DESKTOP"),
-            extractor = ShellTransitionScenarioExtractor(
-                transitionMatcher = object : ITransitionMatcher {
-                    override fun findAll(
-                        transitions: Collection<Transition>
-                    ): Collection<Transition> {
-                        return transitions.filter {
-                            it.type == TransitionType.DESKTOP_MODE_END_DRAG_TO_DESKTOP}
-                    }
-                }),
-            assertions = AssertionTemplates.COMMON_ASSERTIONS +
-                    listOf(
-                        AppLayerIsVisibleAlways(Components.DESKTOP_MODE_APP),
-                        AppWindowOnTopAtEnd(Components.DESKTOP_MODE_APP),
-                        AppWindowHasDesktopModeInitialBoundsAtTheEnd(Components.DESKTOP_MODE_APP)
-                    ).associateBy({ it }, { AssertionInvocationGroup.BLOCKING }),
-        )
-
         @JvmStatic
         @FlickerConfigProvider
         fun flickerConfigProvider(): FlickerConfig =
-            FlickerConfig()
-                    .use(FlickerServiceConfig.DEFAULT)
-                    .use(END_DRAG_TO_DESKTOP)
+            FlickerConfig().use(FlickerServiceConfig.DEFAULT).use(END_DRAG_TO_DESKTOP)
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/CloseAllAppsWithAppHeaderExit.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/CloseAllAppsWithAppHeaderExit.kt
new file mode 100644
index 0000000..0c2b501
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/CloseAllAppsWithAppHeaderExit.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.service.desktopmode.scenarios
+
+import android.app.Instrumentation
+import android.tools.NavBar
+import android.tools.Rotation
+import android.tools.traces.parsers.WindowManagerStateHelper
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import com.android.launcher3.tapl.LauncherInstrumentation
+import com.android.server.wm.flicker.helpers.DesktopModeAppHelper
+import com.android.server.wm.flicker.helpers.MailAppHelper
+import com.android.server.wm.flicker.helpers.NonResizeableAppHelper
+import com.android.server.wm.flicker.helpers.SimpleAppHelper
+import com.android.window.flags.Flags
+import com.android.wm.shell.flicker.service.common.Utils
+import org.junit.After
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+
+@Ignore("Base Test Class")
+abstract class CloseAllAppsWithAppHeaderExit
+@JvmOverloads
+constructor(val rotation: Rotation = Rotation.ROTATION_0) {
+
+    private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+    private val tapl = LauncherInstrumentation()
+    private val wmHelper = WindowManagerStateHelper(instrumentation)
+    private val device = UiDevice.getInstance(instrumentation)
+    private val testApp = DesktopModeAppHelper(SimpleAppHelper(instrumentation))
+    private val mailApp = DesktopModeAppHelper(MailAppHelper(instrumentation))
+    private val nonResizeableApp = DesktopModeAppHelper(NonResizeableAppHelper(instrumentation))
+
+
+
+    @Rule @JvmField val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, rotation)
+
+    @Before
+    fun setup() {
+        Assume.assumeTrue(Flags.enableDesktopWindowingMode())
+        tapl.setEnableRotation(true)
+        tapl.setExpectedRotation(rotation.value)
+        testApp.enterDesktopWithDrag(wmHelper, device)
+        mailApp.launchViaIntent(wmHelper)
+        nonResizeableApp.launchViaIntent(wmHelper)
+    }
+
+    @Test
+    open fun closeAllAppsInDesktop() {
+        nonResizeableApp.closeDesktopApp(wmHelper, device)
+        mailApp.closeDesktopApp(wmHelper, device)
+        testApp.closeDesktopApp(wmHelper, device)
+    }
+
+    @After
+    fun teardown() {
+        testApp.exit(wmHelper)
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/EnterDesktopWithDrag.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/EnterDesktopWithDrag.kt
index 0403b4f..9e9998e 100644
--- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/EnterDesktopWithDrag.kt
+++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/EnterDesktopWithDrag.kt
@@ -23,15 +23,18 @@
 import androidx.test.platform.app.InstrumentationRegistry
 import androidx.test.uiautomator.UiDevice
 import com.android.launcher3.tapl.LauncherInstrumentation
+import com.android.server.wm.flicker.helpers.DesktopModeAppHelper
 import com.android.server.wm.flicker.helpers.SimpleAppHelper
+import com.android.window.flags.Flags
 import com.android.wm.shell.flicker.service.common.Utils
-import com.android.wm.shell.flicker.utils.DesktopModeUtils
 import org.junit.After
+import org.junit.Assume
 import org.junit.Before
 import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 
+
 @Ignore("Base Test Class")
 abstract class EnterDesktopWithDrag
 @JvmOverloads
@@ -41,19 +44,20 @@
     private val tapl = LauncherInstrumentation()
     private val wmHelper = WindowManagerStateHelper(instrumentation)
     private val device = UiDevice.getInstance(instrumentation)
-    private val testApp = SimpleAppHelper(instrumentation)
+    private val testApp = DesktopModeAppHelper(SimpleAppHelper(instrumentation))
 
     @Rule @JvmField val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, rotation)
 
     @Before
     fun setup() {
+        Assume.assumeTrue(Flags.enableDesktopWindowingMode())
         tapl.setEnableRotation(true)
         tapl.setExpectedRotation(rotation.value)
     }
 
     @Test
     open fun enterDesktopWithDrag() {
-        DesktopModeUtils.enterDesktopWithDrag(wmHelper, device, testApp)
+        testApp.enterDesktopWithDrag(wmHelper, device)
     }
 
     @After
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/DesktopModeUtils.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/DesktopModeUtils.kt
deleted file mode 100644
index 345bc5e..0000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/DesktopModeUtils.kt
+++ /dev/null
@@ -1,112 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.flicker.utils
-
-import android.tools.device.apphelpers.StandardAppHelper
-import android.tools.helpers.SYSTEMUI_PACKAGE
-import android.tools.traces.component.IComponentMatcher
-import android.tools.traces.parsers.WindowManagerStateHelper
-import android.tools.traces.wm.WindowingMode
-import androidx.test.uiautomator.By
-import androidx.test.uiautomator.BySelector
-import androidx.test.uiautomator.UiDevice
-import androidx.test.uiautomator.Until
-
-/**
- * Provides a collection of utility functions for desktop mode testing.
- */
-object DesktopModeUtils {
-    private const val TIMEOUT_MS = 3_000L
-    private const val CAPTION = "desktop_mode_caption"
-    private const val CAPTION_HANDLE = "caption_handle"
-    private const val MAXIMIZE_BUTTON = "maximize_button_view"
-
-    private val captionFullscreen: BySelector
-        get() = By.res(SYSTEMUI_PACKAGE, CAPTION)
-    private val captionHandle: BySelector
-        get() = By.res(SYSTEMUI_PACKAGE, CAPTION_HANDLE)
-    private val maximizeButton: BySelector
-        get() = By.res(SYSTEMUI_PACKAGE, MAXIMIZE_BUTTON)
-
-    /**
-     * Wait for an app moved to desktop to finish its transition.
-     */
-    private fun waitForAppToMoveToDesktop(
-        wmHelper: WindowManagerStateHelper,
-        currentApp: IComponentMatcher,
-    ) {
-        wmHelper
-            .StateSyncBuilder()
-            .withWindowSurfaceAppeared(currentApp)
-            .withFreeformApp(currentApp)
-            .withAppTransitionIdle()
-            .waitForAndVerify()
-    }
-
-    /**
-     * Click maximise button on the app header for the given app.
-     */
-    fun maximiseDesktopApp(
-        wmHelper: WindowManagerStateHelper,
-        device: UiDevice,
-        currentApp: StandardAppHelper
-    ) {
-        if (wmHelper.getWindow(currentApp)?.windowingMode
-            != WindowingMode.WINDOWING_MODE_FREEFORM.value)
-            error("expected a freeform window to maximise but window is not in freefrom mode")
-
-        val maximizeButton =
-            device.wait(Until.findObject(maximizeButton), TIMEOUT_MS)
-                ?: error("Unable to find view $maximizeButton\n")
-        maximizeButton.click()
-    }
-
-    /**
-     * Move an app to Desktop by dragging the app handle at the top.
-     */
-    fun enterDesktopWithDrag(
-        wmHelper: WindowManagerStateHelper,
-        device: UiDevice,
-        currentApp: StandardAppHelper,
-    ) {
-        currentApp.launchViaIntent(wmHelper)
-        dragToDesktop(wmHelper, currentApp, device)
-        waitForAppToMoveToDesktop(wmHelper, currentApp)
-    }
-
-    private fun dragToDesktop(
-        wmHelper: WindowManagerStateHelper,
-        currentApp: StandardAppHelper,
-        device: UiDevice
-    ) {
-        val windowRect = wmHelper.getWindowRegion(currentApp).bounds
-        val startX = windowRect.centerX()
-
-        // Start dragging a little under the top to prevent dragging the notification shade.
-        val startY = 10
-
-        val displayRect =
-            wmHelper.currentState.wmState.getDefaultDisplay()?.displayRect
-                ?: throw IllegalStateException("Default display is null")
-
-        // The position we want to drag to
-        val endY = displayRect.centerY() / 2
-
-        // drag the window to move to desktop
-        device.drag(startX, startY, startX, endY, 100)
-    }
-}
diff --git a/libs/hwui/pipeline/skia/ShaderCache.cpp b/libs/hwui/pipeline/skia/ShaderCache.cpp
index b870023..8e07a2f 100644
--- a/libs/hwui/pipeline/skia/ShaderCache.cpp
+++ b/libs/hwui/pipeline/skia/ShaderCache.cpp
@@ -15,14 +15,18 @@
  */
 
 #include "ShaderCache.h"
+
 #include <GrDirectContext.h>
 #include <SkData.h>
 #include <gui/TraceUtils.h>
 #include <log/log.h>
 #include <openssl/sha.h>
+
 #include <algorithm>
 #include <array>
+#include <mutex>
 #include <thread>
+
 #include "FileBlobCache.h"
 #include "Properties.h"
 
diff --git a/libs/hwui/renderthread/HintSessionWrapper.cpp b/libs/hwui/renderthread/HintSessionWrapper.cpp
index 6993d52..7a155c5 100644
--- a/libs/hwui/renderthread/HintSessionWrapper.cpp
+++ b/libs/hwui/renderthread/HintSessionWrapper.cpp
@@ -45,7 +45,7 @@
     LOG_ALWAYS_FATAL_IF(handle_ == nullptr, "Failed to dlopen libandroid.so!");
 
     BIND_APH_METHOD(getManager);
-    BIND_APH_METHOD(createSession);
+    BIND_APH_METHOD(createSessionInternal);
     BIND_APH_METHOD(closeSession);
     BIND_APH_METHOD(updateTargetWorkDuration);
     BIND_APH_METHOD(reportActualWorkDuration);
@@ -122,7 +122,8 @@
     int64_t targetDurationNanos =
             mLastTargetWorkDuration == 0 ? kDefaultTargetDuration : mLastTargetWorkDuration;
     mHintSessionFuture = CommonPool::async([=, this, tids = mPermanentSessionTids] {
-        return mBinding->createSession(manager, tids.data(), tids.size(), targetDurationNanos);
+        return mBinding->createSessionInternal(manager, tids.data(), tids.size(),
+                                               targetDurationNanos, SessionTag::HWUI);
     });
     return false;
 }
diff --git a/libs/hwui/renderthread/HintSessionWrapper.h b/libs/hwui/renderthread/HintSessionWrapper.h
index 14e7a53..859cc57 100644
--- a/libs/hwui/renderthread/HintSessionWrapper.h
+++ b/libs/hwui/renderthread/HintSessionWrapper.h
@@ -17,6 +17,7 @@
 #pragma once
 
 #include <android/performance_hint.h>
+#include <private/performance_hint_private.h>
 
 #include <future>
 #include <optional>
@@ -80,9 +81,10 @@
         virtual ~HintSessionBinding() = default;
         virtual void init();
         APerformanceHintManager* (*getManager)();
-        APerformanceHintSession* (*createSession)(APerformanceHintManager* manager,
-                                                  const int32_t* tids, size_t tidCount,
-                                                  int64_t defaultTarget) = nullptr;
+        APerformanceHintSession* (*createSessionInternal)(APerformanceHintManager* manager,
+                                                          const int32_t* tids, size_t tidCount,
+                                                          int64_t defaultTarget,
+                                                          SessionTag tag) = nullptr;
         void (*closeSession)(APerformanceHintSession* session) = nullptr;
         void (*updateTargetWorkDuration)(APerformanceHintSession* session,
                                          int64_t targetDuration) = nullptr;
diff --git a/libs/hwui/tests/unit/HintSessionWrapperTests.cpp b/libs/hwui/tests/unit/HintSessionWrapperTests.cpp
index c16602c..a8db0f4 100644
--- a/libs/hwui/tests/unit/HintSessionWrapperTests.cpp
+++ b/libs/hwui/tests/unit/HintSessionWrapperTests.cpp
@@ -52,8 +52,8 @@
         void init() override;
 
         MOCK_METHOD(APerformanceHintManager*, fakeGetManager, ());
-        MOCK_METHOD(APerformanceHintSession*, fakeCreateSession,
-                    (APerformanceHintManager*, const int32_t*, size_t, int64_t));
+        MOCK_METHOD(APerformanceHintSession*, fakeCreateSessionInternal,
+                    (APerformanceHintManager*, const int32_t*, size_t, int64_t, SessionTag));
         MOCK_METHOD(void, fakeCloseSession, (APerformanceHintSession*));
         MOCK_METHOD(void, fakeUpdateTargetWorkDuration, (APerformanceHintSession*, int64_t));
         MOCK_METHOD(void, fakeReportActualWorkDuration, (APerformanceHintSession*, int64_t));
@@ -72,22 +72,28 @@
 
     // Must be static so we can point to them as normal fn pointers with HintSessionBinding
     static APerformanceHintManager* stubGetManager() { return sMockBinding->fakeGetManager(); };
-    static APerformanceHintSession* stubCreateSession(APerformanceHintManager* manager,
-                                                      const int32_t* ids, size_t idsSize,
-                                                      int64_t initialTarget) {
-        return sMockBinding->fakeCreateSession(manager, ids, idsSize, initialTarget);
+    static APerformanceHintSession* stubCreateSessionInternal(APerformanceHintManager* manager,
+                                                              const int32_t* ids, size_t idsSize,
+                                                              int64_t initialTarget,
+                                                              SessionTag tag) {
+        return sMockBinding->fakeCreateSessionInternal(manager, ids, idsSize, initialTarget,
+                                                       SessionTag::HWUI);
     }
-    static APerformanceHintSession* stubManagedCreateSession(APerformanceHintManager* manager,
-                                                             const int32_t* ids, size_t idsSize,
-                                                             int64_t initialTarget) {
+    static APerformanceHintSession* stubManagedCreateSessionInternal(
+            APerformanceHintManager* manager, const int32_t* ids, size_t idsSize,
+            int64_t initialTarget, SessionTag tag) {
         sMockBinding->allowCreationToFinish.get_future().wait();
-        return sMockBinding->fakeCreateSession(manager, ids, idsSize, initialTarget);
+        return sMockBinding->fakeCreateSessionInternal(manager, ids, idsSize, initialTarget,
+                                                       SessionTag::HWUI);
     }
-    static APerformanceHintSession* stubSlowCreateSession(APerformanceHintManager* manager,
-                                                          const int32_t* ids, size_t idsSize,
-                                                          int64_t initialTarget) {
+    static APerformanceHintSession* stubSlowCreateSessionInternal(APerformanceHintManager* manager,
+                                                                  const int32_t* ids,
+                                                                  size_t idsSize,
+                                                                  int64_t initialTarget,
+                                                                  SessionTag tag) {
         std::this_thread::sleep_for(50ms);
-        return sMockBinding->fakeCreateSession(manager, ids, idsSize, initialTarget);
+        return sMockBinding->fakeCreateSessionInternal(manager, ids, idsSize, initialTarget,
+                                                       SessionTag::HWUI);
     }
     static void stubCloseSession(APerformanceHintSession* session) {
         sMockBinding->fakeCloseSession(session);
@@ -139,14 +145,14 @@
     mWrapper = std::make_shared<HintSessionWrapper>(uiThreadId, renderThreadId);
     mWrapper->mBinding = sMockBinding;
     EXPECT_CALL(*sMockBinding, fakeGetManager).WillOnce(Return(managerPtr));
-    ON_CALL(*sMockBinding, fakeCreateSession).WillByDefault(Return(sessionPtr));
+    ON_CALL(*sMockBinding, fakeCreateSessionInternal).WillByDefault(Return(sessionPtr));
     ON_CALL(*sMockBinding, fakeSetThreads).WillByDefault(Return(0));
 }
 
 void HintSessionWrapperTests::MockHintSessionBinding::init() {
     sMockBinding->getManager = &stubGetManager;
-    if (sMockBinding->createSession == nullptr) {
-        sMockBinding->createSession = &stubCreateSession;
+    if (sMockBinding->createSessionInternal == nullptr) {
+        sMockBinding->createSessionInternal = &stubCreateSessionInternal;
     }
     sMockBinding->closeSession = &stubCloseSession;
     sMockBinding->updateTargetWorkDuration = &stubUpdateTargetWorkDuration;
@@ -163,14 +169,14 @@
 
 TEST_F(HintSessionWrapperTests, destructorClosesBackgroundSession) {
     EXPECT_CALL(*sMockBinding, fakeCloseSession(sessionPtr)).Times(1);
-    sMockBinding->createSession = stubSlowCreateSession;
+    sMockBinding->createSessionInternal = stubSlowCreateSessionInternal;
     mWrapper->init();
     mWrapper = nullptr;
     Mock::VerifyAndClearExpectations(sMockBinding.get());
 }
 
 TEST_F(HintSessionWrapperTests, sessionInitializesCorrectly) {
-    EXPECT_CALL(*sMockBinding, fakeCreateSession(managerPtr, _, Gt(1), _)).Times(1);
+    EXPECT_CALL(*sMockBinding, fakeCreateSessionInternal(managerPtr, _, Gt(1), _, _)).Times(1);
     mWrapper->init();
     waitForWrapperReady();
 }
@@ -219,7 +225,7 @@
     // Here we test whether queueing delayedDestroy works while creation is still happening, if
     // creation happens after
     EXPECT_CALL(*sMockBinding, fakeCloseSession(sessionPtr)).Times(1);
-    sMockBinding->createSession = &stubManagedCreateSession;
+    sMockBinding->createSessionInternal = &stubManagedCreateSessionInternal;
 
     // Start creating the session and destroying it at the same time
     mWrapper->init();
@@ -246,7 +252,7 @@
     // Here we test whether queueing delayedDestroy works while creation is still happening, if
     // creation happens before
     EXPECT_CALL(*sMockBinding, fakeCloseSession(sessionPtr)).Times(1);
-    sMockBinding->createSession = &stubManagedCreateSession;
+    sMockBinding->createSessionInternal = &stubManagedCreateSessionInternal;
 
     // Start creating the session and destroying it at the same time
     mWrapper->init();
@@ -352,7 +358,7 @@
 }
 
 TEST_F(HintSessionWrapperTests, setThreadsUpdatesSessionThreads) {
-    EXPECT_CALL(*sMockBinding, fakeCreateSession(managerPtr, _, Gt(1), _)).Times(1);
+    EXPECT_CALL(*sMockBinding, fakeCreateSessionInternal(managerPtr, _, Gt(1), _, _)).Times(1);
     EXPECT_CALL(*sMockBinding, fakeSetThreads(sessionPtr, testing::IsSupersetOf({11, 22})))
             .Times(1);
     mWrapper->init();
diff --git a/media/jni/android_media_MediaCodec.cpp b/media/jni/android_media_MediaCodec.cpp
index 8a13c03..4492c85 100644
--- a/media/jni/android_media_MediaCodec.cpp
+++ b/media/jni/android_media_MediaCodec.cpp
@@ -2088,28 +2088,24 @@
             }
             return BAD_VALUE;
         }
-        size_t offset = static_cast<size_t>(env->GetIntField(param, gFields.bufferInfoOffset));
-        size_t size = static_cast<size_t>(env->GetIntField(param, gFields.bufferInfoSize));
+        ssize_t offset = static_cast<ssize_t>(env->GetIntField(param, gFields.bufferInfoOffset));
+        ssize_t size = static_cast<ssize_t>(env->GetIntField(param, gFields.bufferInfoSize));
         uint32_t flags = static_cast<uint32_t>(env->GetIntField(param, gFields.bufferInfoFlags));
-        if (flags == 0 && size == 0) {
+        if (i == 0) {
+            *initialOffset = offset;
+        }
+        if (CC_UNLIKELY((offset < 0)
+                || (size < 0)
+                || ((INT32_MAX - offset) < size)
+                || ((offset - (*initialOffset)) != *totalSize))) {
             if (errorDetailMsg) {
-                *errorDetailMsg = "Error: Queuing an empty BufferInfo";
+                *errorDetailMsg = "Error: offset/size in BufferInfo";
             }
             return BAD_VALUE;
         }
-        if (i == 0) {
-            *initialOffset = offset;
-            if (CC_UNLIKELY(*initialOffset < 0)) {
-                if (errorDetailMsg) {
-                    *errorDetailMsg = "Error: offset/size in BufferInfo";
-                }
-                return BAD_VALUE;
-            }
-        }
-        if (CC_UNLIKELY(((ssize_t)(UINT32_MAX - offset) < (ssize_t)size)
-                || ((offset - *initialOffset) != *totalSize))) {
+        if (flags == 0 && size == 0) {
             if (errorDetailMsg) {
-                *errorDetailMsg = "Error: offset/size in BufferInfo";
+                *errorDetailMsg = "Error: Queuing an empty BufferInfo";
             }
             return BAD_VALUE;
         }
diff --git a/native/android/libandroid.map.txt b/native/android/libandroid.map.txt
index 1c203e3..346c87d 100644
--- a/native/android/libandroid.map.txt
+++ b/native/android/libandroid.map.txt
@@ -366,6 +366,7 @@
     APerformanceHint_setIHintManagerForTesting;
     APerformanceHint_sendHint;
     APerformanceHint_getThreadIds;
+    APerformanceHint_createSessionInternal;
     extern "C++" {
         ASurfaceControl_registerSurfaceStatsListener*;
         ASurfaceControl_unregisterSurfaceStatsListener*;
diff --git a/native/android/performance_hint.cpp b/native/android/performance_hint.cpp
index fbb35e2..83056b2 100644
--- a/native/android/performance_hint.cpp
+++ b/native/android/performance_hint.cpp
@@ -463,6 +463,15 @@
     return manager->createSession(threadIds, size, initialTargetWorkDurationNanos);
 }
 
+APerformanceHintSession* APerformanceHint_createSessionInternal(
+        APerformanceHintManager* manager, const int32_t* threadIds, size_t size,
+        int64_t initialTargetWorkDurationNanos, SessionTag tag) {
+    VALIDATE_PTR(manager)
+    VALIDATE_PTR(threadIds)
+    return manager->createSession(threadIds, size, initialTargetWorkDurationNanos,
+                                  static_cast<hal::SessionTag>(tag));
+}
+
 int64_t APerformanceHint_getPreferredUpdateRateNanos(APerformanceHintManager* manager) {
     VALIDATE_PTR(manager)
     return manager->getPreferredRateNanos();
@@ -486,9 +495,9 @@
     delete session;
 }
 
-int APerformanceHint_sendHint(void* session, SessionHint hint) {
+int APerformanceHint_sendHint(APerformanceHintSession* session, SessionHint hint) {
     VALIDATE_PTR(session)
-    return reinterpret_cast<APerformanceHintSession*>(session)->sendHint(hint);
+    return session->sendHint(hint);
 }
 
 int APerformanceHint_setThreads(APerformanceHintSession* session, const pid_t* threadIds,
@@ -498,11 +507,10 @@
     return session->setThreads(threadIds, size);
 }
 
-int APerformanceHint_getThreadIds(void* aPerformanceHintSession, int32_t* const threadIds,
+int APerformanceHint_getThreadIds(APerformanceHintSession* session, int32_t* const threadIds,
                                   size_t* const size) {
-    VALIDATE_PTR(aPerformanceHintSession)
-    return static_cast<APerformanceHintSession*>(aPerformanceHintSession)
-            ->getThreadIds(threadIds, size);
+    VALIDATE_PTR(session)
+    return session->getThreadIds(threadIds, size);
 }
 
 int APerformanceHint_setPreferPowerEfficiency(APerformanceHintSession* session, bool enabled) {
diff --git a/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp b/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp
index 974e6e6..58f56b8 100644
--- a/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp
+++ b/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp
@@ -30,9 +30,7 @@
 #include <memory>
 #include <vector>
 
-using aidl::android::hardware::power::SessionConfig;
-using aidl::android::hardware::power::SessionTag;
-using aidl::android::hardware::power::WorkDuration;
+namespace hal = aidl::android::hardware::power;
 using aidl::android::os::IHintManager;
 using aidl::android::os::IHintSession;
 using ndk::ScopedAStatus;
@@ -45,7 +43,7 @@
 public:
     MOCK_METHOD(ScopedAStatus, createHintSessionWithConfig,
                 (const SpAIBinder& token, const ::std::vector<int32_t>& tids, int64_t durationNanos,
-                 SessionTag tag, std::optional<SessionConfig>* config,
+                 hal::SessionTag tag, std::optional<hal::SessionConfig>* config,
                  std::shared_ptr<IHintSession>* _aidl_return),
                 (override));
     MOCK_METHOD(ScopedAStatus, getHintSessionPreferredRate, (int64_t * _aidl_return), (override));
@@ -71,7 +69,7 @@
     MOCK_METHOD(ScopedAStatus, setMode, (int32_t mode, bool enabled), (override));
     MOCK_METHOD(ScopedAStatus, close, (), (override));
     MOCK_METHOD(ScopedAStatus, reportActualWorkDuration2,
-                (const ::std::vector<WorkDuration>& workDurations), (override));
+                (const ::std::vector<hal::WorkDuration>& workDurations), (override));
     MOCK_METHOD(SpAIBinder, asBinder, (), (override));
     MOCK_METHOD(bool, isRemote, (), (override));
 };
@@ -95,7 +93,7 @@
         return APerformanceHint_getManager();
     }
     APerformanceHintSession* createSession(APerformanceHintManager* manager,
-                                           int64_t targetDuration = 56789L) {
+                                           int64_t targetDuration = 56789L, bool isHwui = false) {
         mMockSession = ndk::SharedRefBase::make<NiceMock<MockIHintSession>>();
         int64_t sessionId = 123;
         std::vector<int32_t> tids;
@@ -104,8 +102,8 @@
 
         ON_CALL(*mMockIHintManager,
                 createHintSessionWithConfig(_, Eq(tids), Eq(targetDuration), _, _, _))
-                .WillByDefault(DoAll(SetArgPointee<4>(
-                                             std::make_optional<SessionConfig>({.id = sessionId})),
+                .WillByDefault(DoAll(SetArgPointee<4>(std::make_optional<hal::SessionConfig>(
+                                             {.id = sessionId})),
                                      SetArgPointee<5>(std::shared_ptr<IHintSession>(mMockSession)),
                                      [] { return ScopedAStatus::ok(); }));
 
@@ -124,6 +122,10 @@
         ON_CALL(*mMockSession, reportActualWorkDuration2(_)).WillByDefault([] {
             return ScopedAStatus::ok();
         });
+        if (isHwui) {
+            return APerformanceHint_createSessionInternal(manager, tids.data(), tids.size(),
+                                                          targetDuration, SessionTag::HWUI);
+        }
         return APerformanceHint_createSession(manager, tids.data(), tids.size(), targetDuration);
     }
 
@@ -131,7 +133,7 @@
     std::shared_ptr<NiceMock<MockIHintSession>> mMockSession = nullptr;
 };
 
-bool equalsWithoutTimestamp(WorkDuration lhs, WorkDuration rhs) {
+bool equalsWithoutTimestamp(hal::WorkDuration lhs, hal::WorkDuration rhs) {
     return lhs.workPeriodStartTimestampNanos == rhs.workPeriodStartTimestampNanos &&
             lhs.cpuDurationNanos == rhs.cpuDurationNanos &&
             lhs.gpuDurationNanos == rhs.gpuDurationNanos && lhs.durationNanos == rhs.durationNanos;
@@ -194,6 +196,16 @@
     APerformanceHint_closeSession(session);
 }
 
+TEST_F(PerformanceHintTest, TestHwuiSessionCreation) {
+    EXPECT_CALL(*mMockIHintManager,
+                createHintSessionWithConfig(_, _, _, hal::SessionTag::HWUI, _, _))
+            .Times(1);
+    APerformanceHintManager* manager = createManager();
+    APerformanceHintSession* session = createSession(manager, 56789L, true);
+    ASSERT_TRUE(session);
+    APerformanceHint_closeSession(session);
+}
+
 TEST_F(PerformanceHintTest, SetThreads) {
     APerformanceHintManager* manager = createManager();
 
@@ -249,8 +261,8 @@
         return false;
     }
     for (int i = 0; i < expected.size(); ++i) {
-        WorkDuration expectedWorkDuration = expected[i];
-        WorkDuration actualWorkDuration = arg[i];
+        hal::WorkDuration expectedWorkDuration = expected[i];
+        hal::WorkDuration actualWorkDuration = arg[i];
         if (!equalsWithoutTimestamp(expectedWorkDuration, actualWorkDuration)) {
             *result_listener << "WorkDuration at [" << i << "] is different: "
                              << "Expected: " << expectedWorkDuration.toString()
@@ -273,7 +285,7 @@
 
     usleep(2); // Sleep for longer than preferredUpdateRateNanos.
     struct TestPair {
-        WorkDuration duration;
+        hal::WorkDuration duration;
         int expectedResult;
     };
     std::vector<TestPair> testPairs{
@@ -282,7 +294,7 @@
             {{1, -20, 1, 13, -8}, EINVAL},
     };
     for (auto&& pair : testPairs) {
-        std::vector<WorkDuration> actualWorkDurations;
+        std::vector<hal::WorkDuration> actualWorkDurations;
         actualWorkDurations.push_back(pair.duration);
 
         EXPECT_CALL(*mMockSession, reportActualWorkDuration2(WorkDurationEq(actualWorkDurations)))
diff --git a/packages/CrashRecovery/aconfig/flags.aconfig b/packages/CrashRecovery/aconfig/flags.aconfig
index cddbb6b..8cdef38 100644
--- a/packages/CrashRecovery/aconfig/flags.aconfig
+++ b/packages/CrashRecovery/aconfig/flags.aconfig
@@ -16,3 +16,11 @@
     description: "Enables various dependencies of crashrecovery module"
     bug: "289203818"
 }
+
+flag {
+    name: "allow_rescue_party_flag_resets"
+    namespace: "crashrecovery"
+    description: "Enables rescue party flag resets"
+    bug: "287618292"
+    is_fixed_read_only: true
+}
diff --git a/packages/PackageInstaller/AndroidManifest.xml b/packages/PackageInstaller/AndroidManifest.xml
index bf69d3b..4437475 100644
--- a/packages/PackageInstaller/AndroidManifest.xml
+++ b/packages/PackageInstaller/AndroidManifest.xml
@@ -23,6 +23,7 @@
     <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
     <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
     <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED" />
+    <uses-permission android:name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER" />
 
     <uses-permission android:name="com.google.android.permission.INSTALL_WEARABLE_PACKAGES" />
 
diff --git a/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_arrow_drop_down.xml b/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_arrow_drop_down.xml
index 77979f0..8755247 100644
--- a/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_arrow_drop_down.xml
+++ b/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_arrow_drop_down.xml
@@ -16,11 +16,12 @@
   -->
 
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
-        android:viewportWidth="18"
-        android:viewportHeight="18"
-        android:width="24dp"
-        android:height="24dp">
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="@color/settingslib_materialColorOnPrimaryContainer">
     <path
-        android:pathData="M7 10l5 5 5 -5z"
-        android:fillColor="@color/settingslib_materialColorOnPrimaryContainer"/>
+        android:fillColor="@android:color/white"
+        android:pathData="M16.59,8.59L12,13.17 7.41,8.59 6,10l6,6 6,-6 -1.41,-1.41z"/>
 </vector>
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/database/ContentChangeFlow.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/database/ContentChangeFlow.kt
index 8f67354..7ea4b18 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/database/ContentChangeFlow.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/database/ContentChangeFlow.kt
@@ -27,7 +27,7 @@
 import kotlinx.coroutines.flow.flowOn
 
 /** Content change flow for the given [uri]. */
-internal fun Context.contentChangeFlow(
+fun Context.contentChangeFlow(
     uri: Uri,
     sendInitial: Boolean = true,
 ): Flow<Unit> = callbackFlow {
diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml
index a67839a..73c96d9 100644
--- a/packages/SettingsLib/res/values/strings.xml
+++ b/packages/SettingsLib/res/values/strings.xml
@@ -200,6 +200,10 @@
     <string name="bluetooth_active_battery_level">Active. <xliff:g id="battery_level_as_percentage">%1$s</xliff:g> battery.</string>
     <!-- Connected devices settings. Message when Bluetooth is connected and active, showing remote device status and battery level for untethered headset. [CHAR LIMIT=NONE] -->
     <string name="bluetooth_active_battery_level_untethered">Active. L: <xliff:g id="battery_level_as_percentage" example="25%">%1$s</xliff:g>, R: <xliff:g id="battery_level_as_percentage" example="25%">%2$s</xliff:g> battery.</string>
+    <!-- Connected devices settings. Message when Bluetooth is connected and active, showing remote device status and battery level for the left part of the untethered headset. [CHAR LIMIT=NONE] -->
+    <string name="bluetooth_active_battery_level_untethered_left">Active. L: <xliff:g id="battery_level_as_percentage" example="25%">%1$s</xliff:g> battery</string>
+    <!-- Connected devices settings. Message when Bluetooth is connected and active, showing remote device status and battery level  for the right part of the untethered headset. [CHAR LIMIT=NONE] -->
+    <string name="bluetooth_active_battery_level_untethered_right">Active. R: <xliff:g id="battery_level_as_percentage" example="25%">%1$s</xliff:g> battery</string>
     <!-- Connected devices settings. Message when Bluetooth is connected but not in use, showing remote device battery level. [CHAR LIMIT=NONE] -->
     <string name="bluetooth_battery_level"><xliff:g id="battery_level_as_percentage">%1$s</xliff:g> battery</string>
     <!-- Connected devices settings. Message on TV when Bluetooth is connected but not in use, showing remote device battery level. [CHAR LIMIT=NONE] -->
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
index 36a9ecf..a7b7da5 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
@@ -1476,30 +1476,13 @@
                     }
                 }
 
-                // Try to show left/right information if can not get it from battery for hearing
+                // Try to show left/right information for hearing
                 // aids specifically.
                 boolean isActiveAshaHearingAid = mIsActiveDeviceHearingAid;
                 boolean isActiveLeAudioHearingAid = mIsActiveDeviceLeAudio
                         && isConnectedHapClientDevice();
                 if (isActiveAshaHearingAid || isActiveLeAudioHearingAid) {
-                    final Set<CachedBluetoothDevice> memberDevices = getMemberDevice();
-                    final CachedBluetoothDevice subDevice = getSubDevice();
-                    if (memberDevices.stream().anyMatch(m -> m.isConnected())) {
-                        stringRes = R.string.bluetooth_hearing_aid_left_and_right_active;
-                    } else if (subDevice != null && subDevice.isConnected()) {
-                        stringRes = R.string.bluetooth_hearing_aid_left_and_right_active;
-                    } else {
-                        int deviceSide = getDeviceSide();
-                        if (deviceSide == HearingAidInfo.DeviceSide.SIDE_LEFT_AND_RIGHT) {
-                            stringRes = R.string.bluetooth_hearing_aid_left_and_right_active;
-                        } else if (deviceSide == HearingAidInfo.DeviceSide.SIDE_LEFT) {
-                            stringRes = R.string.bluetooth_hearing_aid_left_active;
-                        } else if (deviceSide == HearingAidInfo.DeviceSide.SIDE_RIGHT) {
-                            stringRes = R.string.bluetooth_hearing_aid_right_active;
-                        } else {
-                            stringRes = R.string.bluetooth_active_no_battery_level;
-                        }
-                    }
+                    return getHearingDeviceSummary(leftBattery, rightBattery, shortSummary);
                 }
             }
         }
@@ -1567,6 +1550,62 @@
         return spannableBuilder;
     }
 
+    private CharSequence getHearingDeviceSummary(int leftBattery, int rightBattery,
+            boolean shortSummary) {
+
+        CachedBluetoothDevice memberDevice = getMemberDevice().stream().filter(
+                CachedBluetoothDevice::isConnected).findFirst().orElse(null);
+        if (memberDevice == null && mSubDevice != null && mSubDevice.isConnected()) {
+            memberDevice = mSubDevice;
+        }
+
+        CachedBluetoothDevice leftDevice = null;
+        CachedBluetoothDevice rightDevice = null;
+        final int deviceSide = getDeviceSide();
+        if (deviceSide == HearingAidInfo.DeviceSide.SIDE_LEFT) {
+            leftDevice = this;
+            rightDevice = memberDevice;
+        } else if (deviceSide == HearingAidInfo.DeviceSide.SIDE_RIGHT) {
+            leftDevice = memberDevice;
+            rightDevice = this;
+        } else if (deviceSide == HearingAidInfo.DeviceSide.SIDE_LEFT_AND_RIGHT) {
+            leftDevice = this;
+            rightDevice = this;
+        }
+
+        if (leftBattery < 0 && leftDevice != null) {
+            leftBattery = leftDevice.getBatteryLevel();
+        }
+        if (rightBattery < 0 && rightDevice != null) {
+            rightBattery = rightDevice.getBatteryLevel();
+        }
+
+        if (leftDevice != null && rightDevice != null) {
+            if (leftBattery >= 0 && rightBattery >= 0 && !shortSummary) {
+                return mContext.getString(R.string.bluetooth_active_battery_level_untethered,
+                        Utils.formatPercentage(leftBattery), Utils.formatPercentage(rightBattery));
+            } else {
+                return mContext.getString(R.string.bluetooth_hearing_aid_left_and_right_active);
+            }
+        } else if (leftDevice != null) {
+            if (leftBattery >= 0 && !shortSummary) {
+                return mContext.getString(R.string.bluetooth_active_battery_level_untethered_left,
+                        Utils.formatPercentage(leftBattery));
+            } else {
+                return mContext.getString(R.string.bluetooth_hearing_aid_left_active);
+            }
+        } else if (rightDevice != null) {
+            if (rightBattery >= 0 && !shortSummary) {
+                return mContext.getString(R.string.bluetooth_active_battery_level_untethered_right,
+                        Utils.formatPercentage(rightBattery));
+            } else {
+                return mContext.getString(R.string.bluetooth_hearing_aid_right_active);
+            }
+        }
+
+        return mContext.getString(R.string.bluetooth_active_no_battery_level);
+    }
+
     private void addBatterySpan(SpannableStringBuilder builder,
             String batteryString, boolean lowBattery, int lowBatteryColorRes) {
         if (lowBattery && lowBatteryColorRes != SUMMARY_NO_COLOR_FOR_LOW_BATTERY) {
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index 892d907..6174ab9 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -169,7 +169,6 @@
         "androidx.compose.material_material-icons-extended",
         "androidx.activity_activity-compose",
         "androidx.compose.animation_animation-graphics",
-        "androidx.test.rules",
     ],
     libs: [
         "keepanno-annotations",
@@ -426,6 +425,7 @@
     kotlincflags: ["-Xjvm-default=all"],
     optimize: {
         shrink_resources: false,
+        optimized_shrink_resources: false,
         proguard_flags_files: ["proguard.flags"],
     },
 
@@ -515,6 +515,7 @@
                 optimize: true,
                 shrink: true,
                 shrink_resources: true,
+                optimized_shrink_resources: true,
                 ignore_warnings: false,
                 proguard_compatibility: false,
             },
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 561c85e..5cc3caf 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -1036,6 +1036,7 @@
 
         <receiver
             android:name=".statusbar.KeyboardShortcutsReceiver"
+            android:visibleToInstantApps="true"
             android:exported="true">
             <intent-filter>
                 <action android:name="com.android.intent.action.DISMISS_KEYBOARD_SHORTCUTS" />
@@ -1116,5 +1117,11 @@
                 android:name="android.service.dream"
                 android:resource="@xml/home_controls_dream_metadata" />
         </service>
+
+        <activity android:name="com.android.systemui.keyboard.shortcut.ShortcutHelperActivity"
+            android:exported="false"
+            android:theme="@style/ShortcutHelperTheme"
+            android:excludeFromRecents="true"
+            android:finishOnCloseSystemDialogs="true" />
     </application>
 </manifest>
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index b499a0e..e38f347 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -776,6 +776,13 @@
 }
 
 flag {
+    name: "keyboard_shortcut_helper_rewrite"
+    namespace: "systemui"
+    description: "A new implementation of the keyboards shortcuts helper sheet."
+    bug: "327364197"
+}
+
+flag {
   name: "dream_overlay_bouncer_swipe_direction_filtering"
   namespace: "systemui"
   description: "do not initiate bouncer swipe when the direction is opposite of the expansion"
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt
index 9ce30fd..1e60b98 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt
@@ -599,6 +599,12 @@
             )
         }
 
+        init {
+            // We do this check here to cover all entry points, including Launcher which doesn't
+            // call startIntentWithAnimation()
+            if (!controller.isLaunching) TransitionAnimator.checkReturnAnimationFrameworkFlag()
+        }
+
         @UiThread
         internal fun postTimeouts() {
             if (timeoutHandler != null) {
@@ -637,7 +643,28 @@
                 return
             }
 
-            startAnimation(apps, nonApps, callback)
+            val window = findRootTaskIfPossible(apps)
+            if (window == null) {
+                Log.i(TAG, "Aborting the animation as no window is opening")
+                callback?.invoke()
+
+                if (DEBUG_TRANSITION_ANIMATION) {
+                    Log.d(
+                        TAG,
+                        "Calling controller.onTransitionAnimationCancelled() [no window opening]"
+                    )
+                }
+                controller.onTransitionAnimationCancelled()
+                listener?.onTransitionAnimationCancelled()
+                return
+            }
+
+            val navigationBar =
+                nonApps?.firstOrNull {
+                    it.windowType == WindowManager.LayoutParams.TYPE_NAVIGATION_BAR
+                }
+
+            startAnimation(window, navigationBar, callback)
         }
 
         private fun findRootTaskIfPossible(
@@ -646,9 +673,17 @@
             if (apps == null) {
                 return null
             }
+
+            val targetMode =
+                if (controller.isLaunching) {
+                    RemoteAnimationTarget.MODE_OPENING
+                } else {
+                    RemoteAnimationTarget.MODE_CLOSING
+                }
             var candidate: RemoteAnimationTarget? = null
+
             for (it in apps) {
-                if (it.mode == RemoteAnimationTarget.MODE_OPENING) {
+                if (it.mode == targetMode) {
                     if (activityTransitionUseLargestWindow()) {
                         if (
                             candidate == null ||
@@ -673,47 +708,31 @@
                     }
                 }
             }
+
             return candidate
         }
 
         private fun startAnimation(
-            apps: Array<out RemoteAnimationTarget>?,
-            nonApps: Array<out RemoteAnimationTarget>?,
+            window: RemoteAnimationTarget,
+            navigationBar: RemoteAnimationTarget?,
             iCallback: IRemoteAnimationFinishedCallback?
         ) {
             if (TransitionAnimator.DEBUG) {
                 Log.d(TAG, "Remote animation started")
             }
 
-            val window = findRootTaskIfPossible(apps)
-            if (window == null) {
-                Log.i(TAG, "Aborting the animation as no window is opening")
-                iCallback?.invoke()
-
-                if (DEBUG_TRANSITION_ANIMATION) {
-                    Log.d(
-                        TAG,
-                        "Calling controller.onTransitionAnimationCancelled() [no window opening]"
-                    )
-                }
-                controller.onTransitionAnimationCancelled()
-                listener?.onTransitionAnimationCancelled()
-                return
-            }
-
-            val navigationBar =
-                nonApps?.firstOrNull {
-                    it.windowType == WindowManager.LayoutParams.TYPE_NAVIGATION_BAR
-                }
-
             val windowBounds = window.screenSpaceBounds
             val endState =
-                TransitionAnimator.State(
-                    top = windowBounds.top,
-                    bottom = windowBounds.bottom,
-                    left = windowBounds.left,
-                    right = windowBounds.right
-                )
+                if (controller.isLaunching) {
+                    TransitionAnimator.State(
+                        top = windowBounds.top,
+                        bottom = windowBounds.bottom,
+                        left = windowBounds.left,
+                        right = windowBounds.right
+                    )
+                } else {
+                    controller.createAnimatorState()
+                }
             val windowBackgroundColor =
                 window.taskInfo?.let { callback.getBackgroundColor(it) } ?: window.backgroundColor
 
@@ -721,24 +740,29 @@
             // instead of recomputing isExpandingFullyAbove here.
             val isExpandingFullyAbove =
                 transitionAnimator.isExpandingFullyAbove(controller.transitionContainer, endState)
-            val endRadius =
-                if (isExpandingFullyAbove) {
-                    // Most of the time, expanding fully above the root view means expanding in full
-                    // screen.
-                    ScreenDecorationsUtils.getWindowCornerRadius(context)
-                } else {
-                    // This usually means we are in split screen mode, so 2 out of 4 corners will
-                    // have
-                    // a radius of 0.
-                    0f
-                }
-            endState.topCornerRadius = endRadius
-            endState.bottomCornerRadius = endRadius
+            if (controller.isLaunching) {
+                val endRadius = getWindowRadius(isExpandingFullyAbove)
+                endState.topCornerRadius = endRadius
+                endState.bottomCornerRadius = endRadius
+            }
 
             // We animate the opening window and delegate the view expansion to [this.controller].
             val delegate = this.controller
             val controller =
                 object : Controller by delegate {
+                    override fun createAnimatorState(): TransitionAnimator.State {
+                        if (isLaunching) return delegate.createAnimatorState()
+                        val windowRadius = getWindowRadius(isExpandingFullyAbove)
+                        return TransitionAnimator.State(
+                            top = windowBounds.top,
+                            bottom = windowBounds.bottom,
+                            left = windowBounds.left,
+                            right = windowBounds.right,
+                            topCornerRadius = windowRadius,
+                            bottomCornerRadius = windowRadius
+                        )
+                    }
+
                     override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) {
                         listener?.onTransitionAnimationStart()
 
@@ -795,6 +819,18 @@
                 )
         }
 
+        private fun getWindowRadius(isExpandingFullyAbove: Boolean): Float {
+            return if (isExpandingFullyAbove) {
+                // Most of the time, expanding fully above the root view means
+                // expanding in full screen.
+                ScreenDecorationsUtils.getWindowCornerRadius(context)
+            } else {
+                // This usually means we are in split screen mode, so 2 out of 4
+                // corners will have a radius of 0.
+                0f
+            }
+        }
+
         private fun applyStateToWindow(
             window: RemoteAnimationTarget,
             state: TransitionAnimator.State,
@@ -842,20 +878,41 @@
                 windowCropF.bottom.roundToInt()
             )
 
+            val windowAnimationDelay =
+                if (controller.isLaunching) {
+                    TIMINGS.contentAfterFadeInDelay
+                } else {
+                    TIMINGS.contentBeforeFadeOutDelay
+                }
+            val windowAnimationDuration =
+                if (controller.isLaunching) {
+                    TIMINGS.contentAfterFadeInDuration
+                } else {
+                    TIMINGS.contentBeforeFadeOutDuration
+                }
+            val windowProgress =
+                TransitionAnimator.getProgress(
+                    TIMINGS,
+                    linearProgress,
+                    windowAnimationDelay,
+                    windowAnimationDuration
+                )
+
             // The alpha of the opening window. If it opens above the expandable, then it should
             // fade in progressively. Otherwise, it should be fully opaque and will be progressively
             // revealed as the window background color layer above the window fades out.
             val alpha =
                 if (controller.isBelowAnimatingWindow) {
-                    val windowProgress =
-                        TransitionAnimator.getProgress(
-                            TIMINGS,
-                            linearProgress,
-                            TIMINGS.contentAfterFadeInDelay,
-                            TIMINGS.contentAfterFadeInDuration
+                    if (controller.isLaunching) {
+                        INTERPOLATORS.contentAfterFadeInInterpolator.getInterpolation(
+                            windowProgress
                         )
-
-                    INTERPOLATORS.contentAfterFadeInInterpolator.getInterpolation(windowProgress)
+                    } else {
+                        1 -
+                            INTERPOLATORS.contentBeforeFadeOutInterpolator.getInterpolation(
+                                windowProgress
+                            )
+                    }
                 } else {
                     1f
                 }
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt
index 9bf6b34..679c969 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt
@@ -59,6 +59,13 @@
                 1.0f
             )
         }
+
+        internal fun checkReturnAnimationFrameworkFlag() {
+            check(returnAnimationFrameworkLibrary()) {
+                "isLaunching cannot be false when the returnAnimationFrameworkLibrary flag is " +
+                    "disabled"
+            }
+        }
     }
 
     private val transitionContainerLocation = IntArray(2)
@@ -558,10 +565,4 @@
             }
         }
     }
-
-    private fun checkReturnAnimationFrameworkFlag() {
-        check(returnAnimationFrameworkLibrary()) {
-            "isLaunching cannot be false when the returnAnimationFrameworkLibrary flag is disabled"
-        }
-    }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandlerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandlerTest.java
index 04c4efb..fefe5a0 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandlerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandlerTest.java
@@ -149,7 +149,6 @@
                 mUiEventLogger);
 
         when(mScrimManager.getCurrentController()).thenReturn(mScrimController);
-        when(mCentralSurfaces.isBouncerShowing()).thenReturn(false);
         when(mValueAnimatorCreator.create(anyFloat(), anyFloat())).thenReturn(mValueAnimator);
         when(mVelocityTrackerFactory.obtain()).thenReturn(mVelocityTracker);
         when(mFlingAnimationUtils.getMinVelocityPxPerSecond()).thenReturn(Float.MAX_VALUE);
@@ -193,11 +192,6 @@
                         2)).isTrue();
     }
 
-    private enum Direction {
-        DOWN,
-        UP,
-    }
-
     @Test
     public void testSwipeUp_whenBouncerInitiallyShowing_reduceHeightWithExclusionRects() {
         mTouchHandler.getTouchInitiationRegion(SCREEN_BOUNDS, mRegion,
@@ -210,7 +204,7 @@
                 SCREEN_HEIGHT_PX * MIN_BOUNCER_HEIGHT;
         final int minAllowableBottom = SCREEN_HEIGHT_PX - Math.round(minBouncerHeight);
 
-        expected.set(0, minAllowableBottom , SCREEN_WIDTH_PX, SCREEN_HEIGHT_PX);
+        expected.set(0, minAllowableBottom, SCREEN_WIDTH_PX, SCREEN_HEIGHT_PX);
 
         assertThat(bounds).isEqualTo(expected);
 
@@ -278,69 +272,11 @@
     }
 
     /**
-     * Makes sure swiping up when bouncer initially showing doesn't change the expansion amount.
-     */
-    @DisableFlags(Flags.FLAG_DREAM_OVERLAY_BOUNCER_SWIPE_DIRECTION_FILTERING)
-    @Test
-    public void testSwipeUp_whenBouncerInitiallyShowing_doesNotSetExpansion() {
-        when(mCentralSurfaces.isBouncerShowing()).thenReturn(true);
-
-        mTouchHandler.onSessionStart(mTouchSession);
-        ArgumentCaptor<GestureDetector.OnGestureListener> gestureListenerCaptor =
-                ArgumentCaptor.forClass(GestureDetector.OnGestureListener.class);
-        verify(mTouchSession).registerGestureListener(gestureListenerCaptor.capture());
-
-        final OnGestureListener gestureListener = gestureListenerCaptor.getValue();
-
-        final float percent = .3f;
-        final float distanceY = SCREEN_HEIGHT_PX * percent;
-
-        // Swiping up near the top of the screen where the touch initiation region is.
-        final MotionEvent event1 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE,
-                0, distanceY, 0);
-        final MotionEvent event2 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE,
-                0, 0, 0);
-
-        assertThat(gestureListener.onScroll(event1, event2, 0, distanceY)).isTrue();
-
-        verify(mScrimController, never()).expand(any());
-    }
-
-    /**
-     * Makes sure swiping up when bouncer initially showing doesn't change the expansion amount.
-     */
-    @Test
-    @EnableFlags(Flags.FLAG_DREAM_OVERLAY_BOUNCER_SWIPE_DIRECTION_FILTERING)
-    public void testSwipeUp_whenBouncerInitiallyShowing_doesNotSetExpansion_directionFiltering() {
-        when(mCentralSurfaces.isBouncerShowing()).thenReturn(true);
-
-        mTouchHandler.onSessionStart(mTouchSession);
-        ArgumentCaptor<GestureDetector.OnGestureListener> gestureListenerCaptor =
-                ArgumentCaptor.forClass(GestureDetector.OnGestureListener.class);
-        verify(mTouchSession).registerGestureListener(gestureListenerCaptor.capture());
-
-        final OnGestureListener gestureListener = gestureListenerCaptor.getValue();
-
-        final float percent = .3f;
-        final float distanceY = SCREEN_HEIGHT_PX * percent;
-
-        // Swiping up near the top of the screen where the touch initiation region is.
-        final MotionEvent event1 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE,
-                0, distanceY, 0);
-        final MotionEvent event2 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE,
-                0, 0, 0);
-
-        assertThat(gestureListener.onScroll(event1, event2, 0, distanceY)).isFalse();
-
-        verify(mScrimController, never()).expand(any());
-    }
-
-    /**
-     * Makes sure swiping down when bouncer initially hidden doesn't change the expansion amount.
+     * Makes sure swiping down doesn't change the expansion amount.
      */
     @Test
     @DisableFlags(Flags.FLAG_DREAM_OVERLAY_BOUNCER_SWIPE_DIRECTION_FILTERING)
-    public void testSwipeDown_whenBouncerInitiallyHidden_doesNotSetExpansion() {
+    public void testSwipeDown_doesNotSetExpansion() {
         mTouchHandler.onSessionStart(mTouchSession);
         ArgumentCaptor<GestureDetector.OnGestureListener> gestureListenerCaptor =
                 ArgumentCaptor.forClass(GestureDetector.OnGestureListener.class);
@@ -401,34 +337,8 @@
 
         final OnGestureListener gestureListener = gestureListenerCaptor.getValue();
 
-        verifyScroll(.3f, Direction.UP, false, gestureListener);
-
-        // Ensure that subsequent gestures are treated as expanding even if the bouncer state
-        // changes.
-        when(mCentralSurfaces.isBouncerShowing()).thenReturn(true);
-        verifyScroll(.7f, Direction.UP, false, gestureListener);
-    }
-
-    /**
-     * Makes sure the expansion amount is proportional to scroll.
-     */
-    @Test
-    public void testSwipeDown_setsCorrectExpansionAmount() {
-        when(mCentralSurfaces.isBouncerShowing()).thenReturn(true);
-
-        mTouchHandler.onSessionStart(mTouchSession);
-        ArgumentCaptor<GestureDetector.OnGestureListener> gestureListenerCaptor =
-                ArgumentCaptor.forClass(GestureDetector.OnGestureListener.class);
-        verify(mTouchSession).registerGestureListener(gestureListenerCaptor.capture());
-
-        final OnGestureListener gestureListener = gestureListenerCaptor.getValue();
-
-        verifyScroll(.3f, Direction.DOWN, true, gestureListener);
-
-        // Ensure that subsequent gestures are treated as collapsing even if the bouncer state
-        // changes.
-        when(mCentralSurfaces.isBouncerShowing()).thenReturn(false);
-        verifyScroll(.7f, Direction.DOWN, true, gestureListener);
+        verifyScroll(.3f, gestureListener);
+        verifyScroll(.7f, gestureListener);
     }
 
     /**
@@ -493,25 +403,24 @@
         verify(mCentralSurfaces, never()).awakenDreams();
     }
 
-    private void verifyScroll(float percent, Direction direction,
-            boolean isBouncerInitiallyShowing, GestureDetector.OnGestureListener gestureListener) {
+    private void verifyScroll(float percent,
+            OnGestureListener gestureListener) {
         final float distanceY = SCREEN_HEIGHT_PX * percent;
 
         final MotionEvent event1 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE,
-                0, direction == Direction.UP ? SCREEN_HEIGHT_PX : 0, 0);
+                0, SCREEN_HEIGHT_PX, 0);
         final MotionEvent event2 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE,
-                0, direction == Direction.UP ? SCREEN_HEIGHT_PX - distanceY : distanceY, 0);
+                0, SCREEN_HEIGHT_PX - distanceY, 0);
 
         reset(mScrimController);
         assertThat(gestureListener.onScroll(event1, event2, 0,
-                direction == Direction.UP ? distanceY : -distanceY))
+                distanceY))
                 .isTrue();
 
         // Ensure only called once
         verify(mScrimController).expand(any());
 
-        final float expansion = isBouncerInitiallyShowing ? percent : 1 - percent;
-        final float dragDownAmount = event2.getY() - event1.getY();
+        final float expansion = 1 - percent;
 
         // Ensure correct expansion passed in.
         ShadeExpansionChangeEvent event =
@@ -529,7 +438,7 @@
         final float expansion = 1 - swipeUpPercentage;
         // The upward velocity is ignored.
         final float velocityY = -1;
-        swipeToPosition(swipeUpPercentage, Direction.UP, velocityY);
+        swipeToPosition(swipeUpPercentage, velocityY);
 
         verify(mValueAnimatorCreator).create(eq(expansion),
                 eq(KeyguardBouncerConstants.EXPANSION_HIDDEN));
@@ -552,7 +461,7 @@
         final float expansion = 1 - swipeUpPercentage;
         // The downward velocity is ignored.
         final float velocityY = 1;
-        swipeToPosition(swipeUpPercentage, Direction.UP, velocityY);
+        swipeToPosition(swipeUpPercentage, velocityY);
 
         verify(mValueAnimatorCreator).create(eq(expansion),
                 eq(KeyguardBouncerConstants.EXPANSION_VISIBLE));
@@ -573,57 +482,6 @@
     }
 
     /**
-     * Tests that ending a downward swipe above the set threshold will continue the expansion,
-     * but will not trigger logging of the DREAM_SWIPED event.
-     */
-    @Test
-    public void testSwipeDownPositionAboveThreshold_expandsBouncer_doesNotLog() {
-        when(mCentralSurfaces.isBouncerShowing()).thenReturn(true);
-
-        final float swipeDownPercentage = .3f;
-        // The downward velocity is ignored.
-        final float velocityY = 1;
-        swipeToPosition(swipeDownPercentage, Direction.DOWN, velocityY);
-
-        verify(mValueAnimatorCreator).create(eq(swipeDownPercentage),
-                eq(KeyguardBouncerConstants.EXPANSION_VISIBLE));
-        verify(mValueAnimator, never()).addListener(any());
-
-        verify(mFlingAnimationUtils).apply(eq(mValueAnimator),
-                eq(SCREEN_HEIGHT_PX * swipeDownPercentage),
-                eq(SCREEN_HEIGHT_PX * KeyguardBouncerConstants.EXPANSION_VISIBLE),
-                eq(velocityY), eq((float) SCREEN_HEIGHT_PX));
-        verify(mValueAnimator).start();
-        verify(mUiEventLogger, never()).log(any());
-    }
-
-    /**
-     * Tests that swiping down with a speed above the set threshold leads to bouncer collapsing
-     * down.
-     */
-    @Test
-    public void testSwipeDownVelocityAboveMin_collapsesBouncer() {
-        when(mCentralSurfaces.isBouncerShowing()).thenReturn(true);
-        when(mFlingAnimationUtils.getMinVelocityPxPerSecond()).thenReturn((float) 0);
-
-        // The ending position above the set threshold is ignored.
-        final float swipeDownPercentage = .3f;
-        final float velocityY = 1;
-        swipeToPosition(swipeDownPercentage, Direction.DOWN, velocityY);
-
-        verify(mValueAnimatorCreator).create(eq(swipeDownPercentage),
-                eq(KeyguardBouncerConstants.EXPANSION_HIDDEN));
-        verify(mValueAnimator, never()).addListener(any());
-
-        verify(mFlingAnimationUtilsClosing).apply(eq(mValueAnimator),
-                eq(SCREEN_HEIGHT_PX * swipeDownPercentage),
-                eq(SCREEN_HEIGHT_PX * KeyguardBouncerConstants.EXPANSION_HIDDEN),
-                eq(velocityY), eq((float) SCREEN_HEIGHT_PX));
-        verify(mValueAnimator).start();
-        verify(mUiEventLogger, never()).log(any());
-    }
-
-    /**
      * Tests that swiping up with a speed above the set threshold will continue the expansion.
      */
     @Test
@@ -634,7 +492,7 @@
         final float swipeUpPercentage = .3f;
         final float expansion = 1 - swipeUpPercentage;
         final float velocityY = -1;
-        swipeToPosition(swipeUpPercentage, Direction.UP, velocityY);
+        swipeToPosition(swipeUpPercentage, velocityY);
 
         verify(mValueAnimatorCreator).create(eq(expansion),
                 eq(KeyguardBouncerConstants.EXPANSION_VISIBLE));
@@ -654,26 +512,6 @@
         verify(mUiEventLogger).log(BouncerSwipeTouchHandler.DreamEvent.DREAM_BOUNCER_FULLY_VISIBLE);
     }
 
-    /**
-     * Ensures {@link CentralSurfaces}
-     */
-    @Test
-    public void testInformBouncerShowingOnExpand() {
-        swipeToPosition(1f, Direction.UP, 0);
-    }
-
-    /**
-     * Ensures {@link CentralSurfaces}
-     */
-    @Test
-    public void testInformBouncerHidingOnCollapse() {
-        // Must swipe up to set initial state.
-        swipeToPosition(1f, Direction.UP, 0);
-        Mockito.clearInvocations(mCentralSurfaces);
-
-        swipeToPosition(0f, Direction.DOWN, 0);
-    }
-
     @Test
     public void testTouchSessionOnRemovedCalledTwice() {
         mTouchHandler.onSessionStart(mTouchSession);
@@ -684,7 +522,7 @@
         onRemovedCallbackCaptor.getValue().onRemoved();
     }
 
-    private void swipeToPosition(float percent, Direction direction, float velocityY) {
+    private void swipeToPosition(float percent, float velocityY) {
         Mockito.clearInvocations(mTouchSession);
         mTouchHandler.onSessionStart(mTouchSession);
         ArgumentCaptor<GestureDetector.OnGestureListener> gestureListenerCaptor =
@@ -699,12 +537,12 @@
         final float distanceY = SCREEN_HEIGHT_PX * percent;
 
         final MotionEvent event1 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE,
-                0, direction == Direction.UP ? SCREEN_HEIGHT_PX : 0, 0);
+                0, SCREEN_HEIGHT_PX, 0);
         final MotionEvent event2 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE,
-                0, direction == Direction.UP ? SCREEN_HEIGHT_PX - distanceY : distanceY, 0);
+                0, SCREEN_HEIGHT_PX - distanceY, 0);
 
         assertThat(gestureListenerCaptor.getValue().onScroll(event1, event2, 0,
-                direction == Direction.UP ? distanceY : -distanceY))
+                distanceY))
                 .isTrue();
 
         final MotionEvent upEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/ShadeTouchHandlerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/ShadeTouchHandlerTest.java
index 27bffd0..11a4241 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/ShadeTouchHandlerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/ShadeTouchHandlerTest.java
@@ -18,8 +18,10 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
 
 import android.view.GestureDetector;
 import android.view.MotionEvent;
@@ -28,7 +30,6 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.SysuiTestCase;
-import com.android.systemui.shade.ShadeViewController;
 import com.android.systemui.shared.system.InputChannelCompat;
 import com.android.systemui.statusbar.phone.CentralSurfaces;
 
@@ -36,6 +37,7 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
 import org.mockito.Mock;
 import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
@@ -49,66 +51,89 @@
     CentralSurfaces mCentralSurfaces;
 
     @Mock
-    ShadeViewController mShadeViewController;
-
-    @Mock
     TouchHandler.TouchSession mTouchSession;
 
     ShadeTouchHandler mTouchHandler;
 
+    @Captor
+    ArgumentCaptor<GestureDetector.OnGestureListener> mGestureListenerCaptor;
+    @Captor
+    ArgumentCaptor<InputChannelCompat.InputEventListener> mInputListenerCaptor;
+
     private static final int TOUCH_HEIGHT = 20;
 
     @Before
     public void setup() {
         MockitoAnnotations.initMocks(this);
-        mTouchHandler = new ShadeTouchHandler(Optional.of(mCentralSurfaces), mShadeViewController,
-                TOUCH_HEIGHT);
+
+        mTouchHandler = new ShadeTouchHandler(Optional.of(mCentralSurfaces), TOUCH_HEIGHT);
+    }
+
+    // Verifies that a swipe down in the gesture region is captured by the shade touch handler.
+    @Test
+    public void testSwipeDown_captured() {
+        final boolean captured = swipe(Direction.DOWN);
+
+        assertThat(captured).isTrue();
+    }
+
+    // Verifies that a swipe in the upward direction is not catpured.
+    @Test
+    public void testSwipeUp_notCaptured() {
+        final boolean captured = swipe(Direction.UP);
+
+        // Motion events not captured as the swipe is going in the wrong direction.
+        assertThat(captured).isFalse();
+    }
+
+    // Verifies that a swipe down forwards captured touches to the shade window for handling.
+    @Test
+    public void testSwipeDown_sentToShadeWindow() {
+        swipe(Direction.DOWN);
+
+        // Both motion events are sent for the shade window to process.
+        verify(mCentralSurfaces, times(2)).handleExternalShadeWindowTouch(any());
+    }
+
+    // Verifies that a swipe down is not forwarded to the shade window.
+    @Test
+    public void testSwipeUp_touchesNotSent() {
+        swipe(Direction.UP);
+
+        // Motion events are not sent for the shade window to process as the swipe is going in the
+        // wrong direction.
+        verify(mCentralSurfaces, never()).handleExternalShadeWindowTouch(any());
     }
 
     /**
-     * Verify that touches aren't handled when the bouncer is showing.
+     * Simulates a swipe in the given direction and returns true if the touch was intercepted by the
+     * touch handler's gesture listener.
+     * <p>
+     * Swipe down starts from a Y coordinate of 0 and goes downward. Swipe up starts from the edge
+     * of the gesture region, {@link #TOUCH_HEIGHT}, and goes upward to 0.
      */
-    @Test
-    public void testInactiveOnBouncer() {
-        when(mCentralSurfaces.isBouncerShowing()).thenReturn(true);
+    private boolean swipe(Direction direction) {
+        Mockito.clearInvocations(mTouchSession);
         mTouchHandler.onSessionStart(mTouchSession);
-        verify(mTouchSession).pop();
+
+        verify(mTouchSession).registerGestureListener(mGestureListenerCaptor.capture());
+        verify(mTouchSession).registerInputListener(mInputListenerCaptor.capture());
+
+        final float startY = direction == Direction.UP ? TOUCH_HEIGHT : 0;
+        final float endY = direction == Direction.UP ? 0 : TOUCH_HEIGHT;
+
+        // Send touches to the input and gesture listener.
+        final MotionEvent event1 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE, 0, startY, 0);
+        final MotionEvent event2 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE, 0, endY, 0);
+        mInputListenerCaptor.getValue().onInputEvent(event1);
+        mInputListenerCaptor.getValue().onInputEvent(event2);
+        final boolean captured = mGestureListenerCaptor.getValue().onScroll(event1, event2, 0,
+                startY - endY);
+
+        return captured;
     }
 
-    /**
-     * Make sure {@link ShadeTouchHandler}
-     */
-    @Test
-    public void testTouchPilferingOnScroll() {
-        final MotionEvent motionEvent1 = Mockito.mock(MotionEvent.class);
-        final MotionEvent motionEvent2 = Mockito.mock(MotionEvent.class);
-
-        final ArgumentCaptor<GestureDetector.OnGestureListener> gestureListenerArgumentCaptor =
-                ArgumentCaptor.forClass(GestureDetector.OnGestureListener.class);
-
-        mTouchHandler.onSessionStart(mTouchSession);
-        verify(mTouchSession).registerGestureListener(gestureListenerArgumentCaptor.capture());
-
-        assertThat(gestureListenerArgumentCaptor.getValue()
-                .onScroll(motionEvent1, motionEvent2, 1, 1))
-                .isTrue();
+    private enum Direction {
+        DOWN, UP,
     }
-
-    /**
-     * Ensure touches are propagated to the {@link ShadeViewController}.
-     */
-    @Test
-    public void testEventPropagation() {
-        final MotionEvent motionEvent = Mockito.mock(MotionEvent.class);
-
-        final ArgumentCaptor<InputChannelCompat.InputEventListener>
-                inputEventListenerArgumentCaptor =
-                    ArgumentCaptor.forClass(InputChannelCompat.InputEventListener.class);
-
-        mTouchHandler.onSessionStart(mTouchSession);
-        verify(mTouchSession).registerInputListener(inputEventListenerArgumentCaptor.capture());
-        inputEventListenerArgumentCaptor.getValue().onInputEvent(motionEvent);
-        verify(mShadeViewController).handleExternalTouch(motionEvent);
-    }
-
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt
index 76f15d2..b4b812d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt
@@ -32,7 +32,9 @@
 import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testDispatcher
 import com.android.systemui.kosmos.testScope
+import com.android.systemui.statusbar.notificationShadeWindowController
 import com.android.systemui.testKosmos
 import com.android.systemui.util.settings.fakeSettings
 import com.google.common.truth.Truth.assertThat
@@ -68,8 +70,10 @@
                         keyguardTransitionInteractor = keyguardTransitionInteractor,
                         keyguardInteractor = keyguardInteractor,
                         systemSettings = fakeSettings,
+                        notificationShadeWindowController = notificationShadeWindowController,
                         applicationScope = applicationCoroutineScope,
                         bgScope = applicationCoroutineScope,
+                        mainDispatcher = testDispatcher,
                     )
                     .apply { start() }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java
index 41bc1dc..e2e5169 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java
@@ -16,6 +16,8 @@
 
 package com.android.systemui.dreams;
 
+import static kotlinx.coroutines.flow.FlowKt.emptyFlow;
+
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyFloat;
@@ -28,6 +30,7 @@
 import android.content.res.Resources;
 import android.graphics.Region;
 import android.os.Handler;
+import android.testing.TestableLooper.RunWithLooper;
 import android.view.AttachedSurfaceControl;
 import android.view.ViewGroup;
 import android.view.ViewRootImpl;
@@ -43,6 +46,7 @@
 import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerCallbackInteractor;
 import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerCallbackInteractor.PrimaryBouncerExpansionCallback;
 import com.android.systemui.complication.ComplicationHostViewController;
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
 import com.android.systemui.statusbar.BlurUtils;
 
 import org.junit.Before;
@@ -52,8 +56,11 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import kotlinx.coroutines.CoroutineDispatcher;
+
 @SmallTest
 @RunWith(AndroidJUnit4.class)
+@RunWithLooper(setAsMainLooper = true)
 public class DreamOverlayContainerViewControllerTest extends SysuiTestCase {
     private static final int MAX_BURN_IN_OFFSET = 20;
     private static final long BURN_IN_PROTECTION_UPDATE_INTERVAL = 10;
@@ -87,6 +94,9 @@
     Handler mHandler;
 
     @Mock
+    CoroutineDispatcher mDispatcher;
+
+    @Mock
     BlurUtils mBlurUtils;
 
     @Mock
@@ -103,6 +113,8 @@
 
     @Mock
     DreamOverlayStateController mStateController;
+    @Mock
+    KeyguardTransitionInteractor mKeyguardTransitionInteractor;
 
     DreamOverlayContainerViewController mController;
 
@@ -115,6 +127,7 @@
         when(mDreamOverlayContainerView.getViewRootImpl()).thenReturn(mViewRoot);
         when(mDreamOverlayContainerView.getRootSurfaceControl())
                 .thenReturn(mAttachedSurfaceControl);
+        when(mKeyguardTransitionInteractor.isFinishedInStateWhere(any())).thenReturn(emptyFlow());
 
         mController = new DreamOverlayContainerViewController(
                 mDreamOverlayContainerView,
@@ -124,6 +137,7 @@
                 mLowLightTransitionCoordinator,
                 mBlurUtils,
                 mHandler,
+                mDispatcher,
                 mResources,
                 MAX_BURN_IN_OFFSET,
                 BURN_IN_PROTECTION_UPDATE_INTERVAL,
@@ -131,7 +145,8 @@
                 mPrimaryBouncerCallbackInteractor,
                 mAnimationsController,
                 mStateController,
-                mBouncerlessScrimController);
+                mBouncerlessScrimController,
+                mKeyguardTransitionInteractor);
     }
 
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt
index b18a8ec..bdb0c9a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt
@@ -17,35 +17,52 @@
 
 import android.content.ComponentName
 import android.content.Intent
-import android.os.RemoteException
 import android.service.dreams.IDreamOverlay
 import android.service.dreams.IDreamOverlayCallback
 import android.service.dreams.IDreamOverlayClient
 import android.service.dreams.IDreamOverlayClientCallback
+import android.testing.TestableLooper
 import android.view.View
 import android.view.ViewGroup
 import android.view.WindowManager
 import android.view.WindowManagerImpl
 import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
 import androidx.lifecycle.LifecycleRegistry
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import com.android.compose.animation.scene.ObservableTransitionState
 import com.android.internal.logging.UiEventLogger
 import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.keyguard.KeyguardUpdateMonitorCallback
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.ambient.touch.TouchMonitor
 import com.android.systemui.ambient.touch.dagger.AmbientTouchComponent
+import com.android.systemui.ambient.touch.scrim.ScrimController
+import com.android.systemui.ambient.touch.scrim.ScrimManager
+import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository
+import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository
+import com.android.systemui.communal.data.repository.FakeCommunalRepository
+import com.android.systemui.communal.data.repository.fakeCommunalRepository
+import com.android.systemui.communal.domain.interactor.communalInteractor
+import com.android.systemui.communal.shared.model.CommunalScenes
 import com.android.systemui.complication.ComplicationHostViewController
 import com.android.systemui.complication.ComplicationLayoutEngine
 import com.android.systemui.complication.dagger.ComplicationComponent
 import com.android.systemui.dreams.complication.HideComplicationTouchHandler
 import com.android.systemui.dreams.dagger.DreamOverlayComponent
+import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
 import com.android.systemui.touch.TouchInsetManager
 import com.android.systemui.util.concurrency.FakeExecutor
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.whenever
 import com.android.systemui.util.time.FakeSystemClock
-import com.google.common.truth.Truth
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.test.runCurrent
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -53,20 +70,24 @@
 import org.mockito.Captor
 import org.mockito.Mock
 import org.mockito.Mockito
+import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
-import org.mockito.invocation.InvocationOnMock
 
+@OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
 @RunWith(AndroidJUnit4::class)
 class DreamOverlayServiceTest : SysuiTestCase() {
     private val mFakeSystemClock = FakeSystemClock()
     private val mMainExecutor = FakeExecutor(mFakeSystemClock)
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
 
     @Mock lateinit var mLifecycleOwner: DreamOverlayLifecycleOwner
 
-    @Mock lateinit var mLifecycleRegistry: LifecycleRegistry
+    private lateinit var lifecycleRegistry: FakeLifecycleRegistry
 
-    lateinit var mWindowParams: WindowManager.LayoutParams
+    private lateinit var mWindowParams: WindowManager.LayoutParams
 
     @Mock lateinit var mDreamOverlayCallback: IDreamOverlayCallback
 
@@ -114,18 +135,33 @@
 
     @Mock lateinit var mUiEventLogger: UiEventLogger
 
+    @Mock lateinit var mScrimManager: ScrimManager
+
+    @Mock lateinit var mScrimController: ScrimController
+
+    @Mock lateinit var mSystemDialogsCloser: SystemDialogsCloser
+
     @Mock lateinit var mDreamOverlayCallbackController: DreamOverlayCallbackController
 
+    private lateinit var bouncerRepository: FakeKeyguardBouncerRepository
+    private lateinit var communalRepository: FakeCommunalRepository
+
     @Captor var mViewCaptor: ArgumentCaptor<View>? = null
-    var mService: DreamOverlayService? = null
+    private lateinit var mService: DreamOverlayService
+
     @Before
     fun setup() {
         MockitoAnnotations.initMocks(this)
+
+        lifecycleRegistry = FakeLifecycleRegistry(mLifecycleOwner)
+        bouncerRepository = kosmos.fakeKeyguardBouncerRepository
+        communalRepository = kosmos.fakeCommunalRepository
+
         whenever(mDreamOverlayComponent.getDreamOverlayContainerViewController())
             .thenReturn(mDreamOverlayContainerViewController)
         whenever(mComplicationComponent.getComplicationHostViewController())
             .thenReturn(mComplicationHostViewController)
-        whenever(mLifecycleOwner.registry).thenReturn(mLifecycleRegistry)
+        whenever(mLifecycleOwner.registry).thenReturn(lifecycleRegistry)
         whenever(mComplicationComponentFactory.create(any(), any(), any(), any()))
             .thenReturn(mComplicationComponent)
         whenever(mComplicationComponent.getVisibilityController())
@@ -141,6 +177,7 @@
         whenever(mAmbientTouchComponent.getTouchMonitor()).thenReturn(mTouchMonitor)
         whenever(mDreamOverlayContainerViewController.containerView)
             .thenReturn(mDreamOverlayContainerView)
+        whenever(mScrimManager.getCurrentController()).thenReturn(mScrimController)
         mWindowParams = WindowManager.LayoutParams()
         mService =
             DreamOverlayService(
@@ -154,24 +191,30 @@
                 mAmbientTouchComponentFactory,
                 mStateController,
                 mKeyguardUpdateMonitor,
+                mScrimManager,
+                kosmos.communalInteractor,
+                mSystemDialogsCloser,
                 mUiEventLogger,
                 mTouchInsetManager,
                 LOW_LIGHT_COMPONENT,
                 HOME_CONTROL_PANEL_DREAM_COMPONENT,
                 mDreamOverlayCallbackController,
+                kosmos.keyguardInteractor,
                 WINDOW_NAME
             )
     }
 
-    @get:Throws(RemoteException::class)
-    val client: IDreamOverlayClient
+    private val client: IDreamOverlayClient
         get() {
-            val proxy = mService!!.onBind(Intent())
+            mService.onCreate()
+            TestableLooper.get(this).processAllMessages()
+
+            val proxy = mService.onBind(Intent())
             val overlay = IDreamOverlay.Stub.asInterface(proxy)
             val callback = Mockito.mock(IDreamOverlayClientCallback::class.java)
             overlay.getClient(callback)
             val clientCaptor = ArgumentCaptor.forClass(IDreamOverlayClient::class.java)
-            Mockito.verify(callback).onDreamOverlayClient(clientCaptor.capture())
+            verify(callback).onDreamOverlayClient(clientCaptor.capture())
             return clientCaptor.value
         }
 
@@ -187,9 +230,8 @@
             false /*shouldShowComplication*/
         )
         mMainExecutor.runAllReady()
-        Mockito.verify(mUiEventLogger)
-            .log(DreamOverlayService.DreamOverlayEvent.DREAM_OVERLAY_ENTER_START)
-        Mockito.verify(mUiEventLogger)
+        verify(mUiEventLogger).log(DreamOverlayService.DreamOverlayEvent.DREAM_OVERLAY_ENTER_START)
+        verify(mUiEventLogger)
             .log(DreamOverlayService.DreamOverlayEvent.DREAM_OVERLAY_COMPLETE_START)
     }
 
@@ -205,7 +247,7 @@
             false /*shouldShowComplication*/
         )
         mMainExecutor.runAllReady()
-        Mockito.verify(mWindowManager).addView(any(), any())
+        verify(mWindowManager).addView(any(), any())
     }
 
     // Validates that {@link DreamOverlayService} properly handles the case where the dream's
@@ -224,14 +266,14 @@
             false /*shouldShowComplication*/
         )
         mMainExecutor.runAllReady()
-        Mockito.verify(mWindowManager).addView(any(), any())
-        Mockito.verify(mStateController).setOverlayActive(false)
-        Mockito.verify(mStateController).setLowLightActive(false)
-        Mockito.verify(mStateController).setEntryAnimationsFinished(false)
-        Mockito.verify(mStateController, Mockito.never()).setOverlayActive(true)
-        Mockito.verify(mUiEventLogger, Mockito.never())
+        verify(mWindowManager).addView(any(), any())
+        verify(mStateController).setOverlayActive(false)
+        verify(mStateController).setLowLightActive(false)
+        verify(mStateController).setEntryAnimationsFinished(false)
+        verify(mStateController, Mockito.never()).setOverlayActive(true)
+        verify(mUiEventLogger, Mockito.never())
             .log(DreamOverlayService.DreamOverlayEvent.DREAM_OVERLAY_COMPLETE_START)
-        Mockito.verify(mDreamOverlayCallbackController, Mockito.never()).onStartDream()
+        verify(mDreamOverlayCallbackController, Mockito.never()).onStartDream()
     }
 
     @Test
@@ -246,7 +288,7 @@
             false /*shouldShowComplication*/
         )
         mMainExecutor.runAllReady()
-        Mockito.verify(mDreamOverlayContainerViewController).init()
+        verify(mDreamOverlayContainerViewController).init()
     }
 
     @Test
@@ -264,7 +306,7 @@
             false /*shouldShowComplication*/
         )
         mMainExecutor.runAllReady()
-        Mockito.verify(mDreamOverlayContainerViewParent).removeView(mDreamOverlayContainerView)
+        verify(mDreamOverlayContainerViewParent).removeView(mDreamOverlayContainerView)
     }
 
     @Test
@@ -279,7 +321,7 @@
             true /*shouldShowComplication*/
         )
         mMainExecutor.runAllReady()
-        Truth.assertThat(mService!!.shouldShowComplications()).isTrue()
+        assertThat(mService.shouldShowComplications()).isTrue()
     }
 
     @Test
@@ -294,8 +336,8 @@
             false /*shouldShowComplication*/
         )
         mMainExecutor.runAllReady()
-        Truth.assertThat(mService!!.dreamComponent).isEqualTo(LOW_LIGHT_COMPONENT)
-        Mockito.verify(mStateController).setLowLightActive(true)
+        assertThat(mService.dreamComponent).isEqualTo(LOW_LIGHT_COMPONENT)
+        verify(mStateController).setLowLightActive(true)
     }
 
     @Test
@@ -310,8 +352,8 @@
             false /*shouldShowComplication*/
         )
         mMainExecutor.runAllReady()
-        Truth.assertThat(mService!!.dreamComponent).isEqualTo(HOME_CONTROL_PANEL_DREAM_COMPONENT)
-        Mockito.verify(mStateController).setHomeControlPanelActive(true)
+        assertThat(mService.dreamComponent).isEqualTo(HOME_CONTROL_PANEL_DREAM_COMPONENT)
+        verify(mStateController).setHomeControlPanelActive(true)
     }
 
     @Test
@@ -328,19 +370,19 @@
         mMainExecutor.runAllReady()
 
         // Verify view added.
-        Mockito.verify(mWindowManager).addView(mViewCaptor!!.capture(), any())
+        verify(mWindowManager).addView(mViewCaptor!!.capture(), any())
 
         // Service destroyed.
-        mService!!.onEndDream()
+        mService.onEndDream()
         mMainExecutor.runAllReady()
 
         // Verify view removed.
-        Mockito.verify(mWindowManager).removeView(mViewCaptor!!.value)
+        verify(mWindowManager).removeView(mViewCaptor!!.value)
 
         // Verify state correctly set.
-        Mockito.verify(mStateController).setOverlayActive(false)
-        Mockito.verify(mStateController).setLowLightActive(false)
-        Mockito.verify(mStateController).setEntryAnimationsFinished(false)
+        verify(mStateController).setOverlayActive(false)
+        verify(mStateController).setLowLightActive(false)
+        verify(mStateController).setEntryAnimationsFinished(false)
     }
 
     @Test
@@ -373,7 +415,7 @@
 
         // Schedule the endDream call in the middle of the startDream implementation, as any
         // ordering is possible.
-        Mockito.doAnswer { invocation: InvocationOnMock? ->
+        Mockito.doAnswer {
                 client.endDream()
                 null
             }
@@ -409,37 +451,37 @@
         mMainExecutor.runAllReady()
 
         // Verify view added.
-        Mockito.verify(mWindowManager).addView(mViewCaptor!!.capture(), any())
+        verify(mWindowManager).addView(mViewCaptor!!.capture(), any())
 
         // Service destroyed.
-        mService!!.onDestroy()
+        mService.onDestroy()
         mMainExecutor.runAllReady()
 
         // Verify view removed.
-        Mockito.verify(mWindowManager).removeView(mViewCaptor!!.value)
+        verify(mWindowManager).removeView(mViewCaptor!!.value)
 
         // Verify state correctly set.
-        Mockito.verify(mKeyguardUpdateMonitor).removeCallback(any())
-        Mockito.verify(mLifecycleRegistry).currentState = Lifecycle.State.DESTROYED
-        Mockito.verify(mStateController).setOverlayActive(false)
-        Mockito.verify(mStateController).setLowLightActive(false)
-        Mockito.verify(mStateController).setEntryAnimationsFinished(false)
+        verify(mKeyguardUpdateMonitor).removeCallback(any())
+        assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.DESTROYED)
+        verify(mStateController).setOverlayActive(false)
+        verify(mStateController).setLowLightActive(false)
+        verify(mStateController).setEntryAnimationsFinished(false)
     }
 
     @Test
     fun testDoNotRemoveViewOnDestroyIfOverlayNotStarted() {
         // Service destroyed without ever starting dream.
-        mService!!.onDestroy()
+        mService.onDestroy()
         mMainExecutor.runAllReady()
 
         // Verify no view is removed.
-        Mockito.verify(mWindowManager, Mockito.never()).removeView(any())
+        verify(mWindowManager, Mockito.never()).removeView(any())
 
         // Verify state still correctly set.
-        Mockito.verify(mKeyguardUpdateMonitor).removeCallback(any())
-        Mockito.verify(mLifecycleRegistry).currentState = Lifecycle.State.DESTROYED
-        Mockito.verify(mStateController).setOverlayActive(false)
-        Mockito.verify(mStateController).setLowLightActive(false)
+        verify(mKeyguardUpdateMonitor).removeCallback(any())
+        assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.DESTROYED)
+        verify(mStateController).setOverlayActive(false)
+        verify(mStateController).setLowLightActive(false)
     }
 
     @Test
@@ -447,7 +489,7 @@
         val client = client
 
         // Destroy the service.
-        mService!!.onDestroy()
+        mService.onDestroy()
         mMainExecutor.runAllReady()
 
         // Inform the overlay service of dream starting.
@@ -458,15 +500,15 @@
             false /*shouldShowComplication*/
         )
         mMainExecutor.runAllReady()
-        Mockito.verify(mWindowManager, Mockito.never()).addView(any(), any())
+        verify(mWindowManager, Mockito.never()).addView(any(), any())
     }
 
     @Test
     fun testNeverRemoveDecorViewIfNotAdded() {
         // Service destroyed before dream started.
-        mService!!.onDestroy()
+        mService.onDestroy()
         mMainExecutor.runAllReady()
-        Mockito.verify(mWindowManager, Mockito.never()).removeView(any())
+        verify(mWindowManager, Mockito.never()).removeView(any())
     }
 
     @Test
@@ -483,11 +525,11 @@
         mMainExecutor.runAllReady()
 
         // Verify that a new window is added.
-        Mockito.verify(mWindowManager).addView(mViewCaptor!!.capture(), any())
+        verify(mWindowManager).addView(mViewCaptor!!.capture(), any())
         val windowDecorView = mViewCaptor!!.value
 
         // Assert that the overlay is not showing complications.
-        Truth.assertThat(mService!!.shouldShowComplications()).isFalse()
+        assertThat(mService.shouldShowComplications()).isFalse()
         Mockito.clearInvocations(mDreamOverlayComponent)
         Mockito.clearInvocations(mAmbientTouchComponent)
         Mockito.clearInvocations(mWindowManager)
@@ -504,16 +546,16 @@
         mMainExecutor.runAllReady()
 
         // Assert that the overlay is showing complications.
-        Truth.assertThat(mService!!.shouldShowComplications()).isTrue()
+        assertThat(mService.shouldShowComplications()).isTrue()
 
         // Verify that the old overlay window has been removed, and a new one created.
-        Mockito.verify(mWindowManager).removeView(windowDecorView)
-        Mockito.verify(mWindowManager).addView(any(), any())
+        verify(mWindowManager).removeView(windowDecorView)
+        verify(mWindowManager).addView(any(), any())
 
         // Verify that new instances of overlay container view controller and overlay touch monitor
         // are created.
-        Mockito.verify(mDreamOverlayComponent).getDreamOverlayContainerViewController()
-        Mockito.verify(mAmbientTouchComponent).getTouchMonitor()
+        verify(mDreamOverlayComponent).getDreamOverlayContainerViewController()
+        verify(mAmbientTouchComponent).getTouchMonitor()
     }
 
     @Test
@@ -528,15 +570,15 @@
             true /*shouldShowComplication*/
         )
         mMainExecutor.runAllReady()
-        mService!!.onWakeUp()
-        Mockito.verify(mDreamOverlayContainerViewController).wakeUp()
-        Mockito.verify(mDreamOverlayCallbackController).onWakeUp()
+        mService.onWakeUp()
+        verify(mDreamOverlayContainerViewController).wakeUp()
+        verify(mDreamOverlayCallbackController).onWakeUp()
     }
 
     @Test
     fun testWakeUpBeforeStartDoesNothing() {
-        mService!!.onWakeUp()
-        Mockito.verify(mDreamOverlayContainerViewController, Mockito.never()).wakeUp()
+        mService.onWakeUp()
+        verify(mDreamOverlayContainerViewController, Mockito.never()).wakeUp()
     }
 
     @Test
@@ -554,8 +596,8 @@
         val paramsCaptor = ArgumentCaptor.forClass(WindowManager.LayoutParams::class.java)
 
         // Verify that a new window is added.
-        Mockito.verify(mWindowManager).addView(any(), paramsCaptor.capture())
-        Truth.assertThat(
+        verify(mWindowManager).addView(any(), paramsCaptor.capture())
+        assertThat(
                 paramsCaptor.value.privateFlags and
                     WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS ==
                     WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS
@@ -563,6 +605,254 @@
             .isTrue()
     }
 
+    // Tests that the bouncer closes when DreamOverlayService is told that the dream is coming to
+    // the front.
+    @Test
+    fun testBouncerRetractedWhenDreamComesToFront() {
+        val client = client
+
+        // Inform the overlay service of dream starting.
+        client.startDream(
+            mWindowParams,
+            mDreamOverlayCallback,
+            DREAM_COMPONENT,
+            true /*shouldShowComplication*/
+        )
+        mMainExecutor.runAllReady()
+
+        whenever(mDreamOverlayContainerViewController.isBouncerShowing()).thenReturn(true)
+        mService!!.onComeToFront()
+        verify(mScrimController).expand(any())
+    }
+
+    // Tests that glanceable hub is hidden when DreamOverlayService is told that the dream is
+    // coming to the front.
+    @Test
+    fun testGlanceableHubHiddenWhenDreamComesToFront() {
+        val client = client
+
+        // Inform the overlay service of dream starting.
+        client.startDream(
+            mWindowParams,
+            mDreamOverlayCallback,
+            DREAM_COMPONENT,
+            true /*shouldShowComplication*/
+        )
+        mMainExecutor.runAllReady()
+
+        mService!!.onComeToFront()
+        assertThat(communalRepository.currentScene.value).isEqualTo(CommunalScenes.Blank)
+    }
+
+    // Tests that system dialogs (e.g. notification shade) closes when DreamOverlayService is told
+    // that the dream is coming to the front.
+    @Test
+    fun testSystemDialogsClosedWhenDreamComesToFront() {
+        val client = client
+
+        // Inform the overlay service of dream starting.
+        client.startDream(
+            mWindowParams,
+            mDreamOverlayCallback,
+            DREAM_COMPONENT,
+            true /*shouldShowComplication*/
+        )
+        mMainExecutor.runAllReady()
+
+        mService!!.onComeToFront()
+        verify(mSystemDialogsCloser).closeSystemDialogs()
+    }
+
+    @Test
+    fun testLifecycle_createdAfterConstruction() {
+        mMainExecutor.runAllReady()
+        assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.CREATED)
+    }
+
+    @Test
+    fun testLifecycle_resumedAfterDreamStarts() {
+        val client = client
+
+        // Inform the overlay service of dream starting. Do not show dream complications.
+        client.startDream(
+            mWindowParams,
+            mDreamOverlayCallback,
+            DREAM_COMPONENT,
+            false /*shouldShowComplication*/
+        )
+        mMainExecutor.runAllReady()
+        assertThat(lifecycleRegistry.mLifecycles)
+            .containsExactly(
+                Lifecycle.State.CREATED,
+                Lifecycle.State.STARTED,
+                Lifecycle.State.RESUMED
+            )
+    }
+
+    @Test
+    fun testLifecycle_destroyedAfterOnDestroy() {
+        val client = client
+
+        // Inform the overlay service of dream starting. Do not show dream complications.
+        client.startDream(
+            mWindowParams,
+            mDreamOverlayCallback,
+            DREAM_COMPONENT,
+            false /*shouldShowComplication*/
+        )
+        mMainExecutor.runAllReady()
+        mService.onDestroy()
+        mMainExecutor.runAllReady()
+        assertThat(lifecycleRegistry.mLifecycles)
+            .containsExactly(
+                Lifecycle.State.CREATED,
+                Lifecycle.State.STARTED,
+                Lifecycle.State.RESUMED,
+                Lifecycle.State.DESTROYED
+            )
+    }
+
+    @Test
+    fun testNotificationShadeShown_setsLifecycleState() {
+        val client = client
+
+        // Inform the overlay service of dream starting. Do not show dream complications.
+        client.startDream(
+            mWindowParams,
+            mDreamOverlayCallback,
+            DREAM_COMPONENT,
+            false /*shouldShowComplication*/
+        )
+        mMainExecutor.runAllReady()
+        assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.RESUMED)
+        val callbackCaptor = ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback::class.java)
+        verify(mKeyguardUpdateMonitor).registerCallback(callbackCaptor.capture())
+
+        // Notification shade opens.
+        callbackCaptor.value.onShadeExpandedChanged(true)
+        mMainExecutor.runAllReady()
+
+        // Lifecycle state goes from resumed back to started when the notification shade shows.
+        assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.STARTED)
+
+        // Notification shade closes.
+        callbackCaptor.value.onShadeExpandedChanged(false)
+        mMainExecutor.runAllReady()
+
+        // Lifecycle state goes back to RESUMED.
+        assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.RESUMED)
+    }
+
+    @Test
+    fun testBouncerShown_setsLifecycleState() {
+        val client = client
+
+        // Inform the overlay service of dream starting. Do not show dream complications.
+        client.startDream(
+            mWindowParams,
+            mDreamOverlayCallback,
+            DREAM_COMPONENT,
+            false /*shouldShowComplication*/
+        )
+        mMainExecutor.runAllReady()
+        assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.RESUMED)
+
+        // Bouncer shows.
+        bouncerRepository.setPrimaryShow(true)
+        testScope.runCurrent()
+        mMainExecutor.runAllReady()
+
+        // Lifecycle state goes from resumed back to started when the notification shade shows.
+        assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.STARTED)
+
+        // Bouncer closes.
+        bouncerRepository.setPrimaryShow(false)
+        testScope.runCurrent()
+        mMainExecutor.runAllReady()
+
+        // Lifecycle state goes back to RESUMED.
+        assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.RESUMED)
+    }
+
+    @Test
+    fun testCommunalVisible_setsLifecycleState() {
+        val client = client
+
+        // Inform the overlay service of dream starting. Do not show dream complications.
+        client.startDream(
+            mWindowParams,
+            mDreamOverlayCallback,
+            DREAM_COMPONENT,
+            false /*shouldShowComplication*/
+        )
+        mMainExecutor.runAllReady()
+        assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.RESUMED)
+        val transitionState: MutableStateFlow<ObservableTransitionState> =
+            MutableStateFlow(ObservableTransitionState.Idle(CommunalScenes.Blank))
+        communalRepository.setTransitionState(transitionState)
+
+        // Communal becomes visible.
+        transitionState.value = ObservableTransitionState.Idle(CommunalScenes.Communal)
+        testScope.runCurrent()
+        mMainExecutor.runAllReady()
+
+        // Lifecycle state goes from resumed back to started when the notification shade shows.
+        assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.STARTED)
+
+        // Communal closes.
+        transitionState.value = ObservableTransitionState.Idle(CommunalScenes.Blank)
+        testScope.runCurrent()
+        mMainExecutor.runAllReady()
+
+        // Lifecycle state goes back to RESUMED.
+        assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.RESUMED)
+    }
+
+    // Verifies the dream's lifecycle
+    @Test
+    fun testLifecycleStarted_whenAnyOcclusion() {
+        val client = client
+
+        // Inform the overlay service of dream starting. Do not show dream complications.
+        client.startDream(
+            mWindowParams,
+            mDreamOverlayCallback,
+            DREAM_COMPONENT,
+            false /*shouldShowComplication*/
+        )
+        mMainExecutor.runAllReady()
+        assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.RESUMED)
+        val transitionState: MutableStateFlow<ObservableTransitionState> =
+            MutableStateFlow(ObservableTransitionState.Idle(CommunalScenes.Blank))
+        communalRepository.setTransitionState(transitionState)
+
+        // Communal becomes visible.
+        transitionState.value = ObservableTransitionState.Idle(CommunalScenes.Communal)
+        testScope.runCurrent()
+        mMainExecutor.runAllReady()
+
+        // Lifecycle state goes from resumed back to started when the notification shade shows.
+        assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.STARTED)
+
+        // Communal closes.
+        transitionState.value = ObservableTransitionState.Idle(CommunalScenes.Blank)
+        testScope.runCurrent()
+        mMainExecutor.runAllReady()
+
+        // Lifecycle state goes back to RESUMED.
+        assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.RESUMED)
+    }
+
+    internal class FakeLifecycleRegistry(provider: LifecycleOwner) : LifecycleRegistry(provider) {
+        val mLifecycles: MutableList<State> = ArrayList()
+
+        override var currentState: State
+            get() = mLifecycles[mLifecycles.size - 1]
+            set(state) {
+                mLifecycles.add(state)
+            }
+    }
+
     companion object {
         private val LOW_LIGHT_COMPONENT = ComponentName("package", "lowlight")
         private val HOME_CONTROL_PANEL_DREAM_COMPONENT =
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..e3c6dee 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).handleExternalShadeWindowTouch(motionEvent);
     }
 
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/slider/SeekableSliderHapticPluginTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/slider/SeekbarHapticPluginTest.kt
similarity index 96%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/slider/SeekableSliderHapticPluginTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/slider/SeekbarHapticPluginTest.kt
index 805b4a8..855b6d0 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/slider/SeekableSliderHapticPluginTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/slider/SeekbarHapticPluginTest.kt
@@ -44,14 +44,14 @@
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 @OptIn(ExperimentalCoroutinesApi::class)
-class SeekableSliderHapticPluginTest : SysuiTestCase() {
+class SeekbarHapticPluginTest : SysuiTestCase() {
 
     private val kosmos = Kosmos()
 
     @Rule @JvmField val mMockitoRule: MockitoRule = MockitoJUnit.rule()
     @Mock private lateinit var vibratorHelper: VibratorHelper
     private val seekBar = SeekBar(mContext)
-    private lateinit var plugin: SeekableSliderHapticPlugin
+    private lateinit var plugin: SeekbarHapticPlugin
 
     @Before
     fun setup() {
@@ -142,7 +142,7 @@
 
     private fun createPlugin() {
         plugin =
-            SeekableSliderHapticPlugin(
+            SeekbarHapticPlugin(
                 vibratorHelper,
                 kosmos.fakeSystemClock,
             )
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/slider/SliderStateProducerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/slider/SliderStateProducerTest.kt
new file mode 100644
index 0000000..88189db
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/slider/SliderStateProducerTest.kt
@@ -0,0 +1,134 @@
+/*
+ * 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.haptics.slider
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import junit.framework.Assert.assertEquals
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class SliderStateProducerTest : SysuiTestCase() {
+
+    private val eventProducer = SliderStateProducer()
+    private val eventFlow = eventProducer.produceEvents()
+
+    @Test
+    fun onStartTrackingTouch_noProgress_trackingTouchEventProduced() = runTest {
+        val latest by collectLastValue(eventFlow)
+
+        eventProducer.onStartTracking(/*fromUser =*/ true)
+
+        assertEquals(SliderEvent(SliderEventType.STARTED_TRACKING_TOUCH, 0F), latest)
+    }
+
+    @Test
+    fun onStopTrackingTouch_noProgress_StoppedTrackingTouchEventProduced() = runTest {
+        val latest by collectLastValue(eventFlow)
+
+        eventProducer.onStopTracking(/*fromUser =*/ true)
+
+        assertEquals(SliderEvent(SliderEventType.STOPPED_TRACKING_TOUCH, 0F), latest)
+    }
+
+    @Test
+    fun onStartTrackingProgram_noProgress_trackingTouchEventProduced() = runTest {
+        val latest by collectLastValue(eventFlow)
+
+        eventProducer.onStartTracking(/*fromUser =*/ false)
+
+        assertEquals(SliderEvent(SliderEventType.STARTED_TRACKING_PROGRAM, 0F), latest)
+    }
+
+    @Test
+    fun onStopTrackingProgram_noProgress_StoppedTrackingTouchEventProduced() = runTest {
+        val latest by collectLastValue(eventFlow)
+
+        eventProducer.onStopTracking(/*fromUser =*/ false)
+
+        assertEquals(SliderEvent(SliderEventType.STOPPED_TRACKING_PROGRAM, 0F), latest)
+    }
+
+    @Test
+    fun onProgressChangeByUser_changeByUserEventProduced() = runTest {
+        val progress = 0.5f
+        val latest by collectLastValue(eventFlow)
+
+        eventProducer.onProgressChanged(/*fromUser =*/ true, progress)
+
+        assertEquals(SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_USER, progress), latest)
+    }
+
+    @Test
+    fun onProgressChangeByProgram_changeByProgramEventProduced() = runTest {
+        val progress = 0.5f
+        val latest by collectLastValue(eventFlow)
+
+        eventProducer.onProgressChanged(/*fromUser =*/ false, progress)
+
+        assertEquals(SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_PROGRAM, progress), latest)
+    }
+
+    @Test
+    fun onStartTrackingTouch_afterProgress_trackingTouchEventProduced() = runTest {
+        val progress = 0.5f
+        val latest by collectLastValue(eventFlow)
+
+        eventProducer.onProgressChanged(/*fromUser =*/ true, progress)
+        eventProducer.onStartTracking(/*fromUser =*/ true)
+
+        assertEquals(SliderEvent(SliderEventType.STARTED_TRACKING_TOUCH, progress), latest)
+    }
+
+    @Test
+    fun onStopTrackingTouch_afterProgress_stopTrackingTouchEventProduced() = runTest {
+        val progress = 0.5f
+        val latest by collectLastValue(eventFlow)
+
+        eventProducer.onProgressChanged(/*fromUser =*/ true, progress)
+        eventProducer.onStopTracking(/*fromUser =*/ true)
+
+        assertEquals(SliderEvent(SliderEventType.STOPPED_TRACKING_TOUCH, progress), latest)
+    }
+
+    @Test
+    fun onStartTrackingProgram_afterProgress_trackingProgramEventProduced() = runTest {
+        val progress = 0.5f
+        val latest by collectLastValue(eventFlow)
+
+        eventProducer.onProgressChanged(/*fromUser =*/ false, progress)
+        eventProducer.onStartTracking(/*fromUser =*/ false)
+
+        assertEquals(SliderEvent(SliderEventType.STARTED_TRACKING_PROGRAM, progress), latest)
+    }
+
+    @Test
+    fun onStopTrackingProgram_afterProgress_stopTrackingProgramEventProduced() = runTest {
+        val progress = 0.5f
+        val latest by collectLastValue(eventFlow)
+
+        eventProducer.onProgressChanged(/*fromUser =*/ false, progress)
+        eventProducer.onStopTracking(/*fromUser =*/ false)
+
+        assertEquals(SliderEvent(SliderEventType.STOPPED_TRACKING_PROGRAM, progress), latest)
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/shared/flag/SceneContainerFlagParameterizationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/shared/flag/SceneContainerFlagParameterizationTest.kt
index db31ad5..4d69f0d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/shared/flag/SceneContainerFlagParameterizationTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/shared/flag/SceneContainerFlagParameterizationTest.kt
@@ -24,6 +24,7 @@
 import com.android.systemui.Flags.FLAG_SCENE_CONTAINER
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.flags.andSceneContainer
+import com.android.systemui.flags.parameterizeSceneContainerFlag
 import com.google.common.truth.Truth
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -41,6 +42,14 @@
     }
 
     @Test
+    fun parameterizeSceneContainer() {
+        val result = parameterizeSceneContainerFlag()
+        Truth.assertThat(result).hasSize(2)
+        Truth.assertThat(result[0].mOverrides[FLAG_SCENE_CONTAINER]).isFalse()
+        Truth.assertThat(result[1].mOverrides[FLAG_SCENE_CONTAINER]).isTrue()
+    }
+
+    @Test
     fun oneUnrelatedAndSceneContainer() {
         val unrelatedFlag = FLAG_EXAMPLE_FLAG
         val result = FlagsParameterization.allCombinationsOf(unrelatedFlag).andSceneContainer()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
similarity index 77%
rename from packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
index b89ccef..a8da116 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
@@ -29,7 +29,6 @@
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.clearInvocations;
-import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.spy;
@@ -37,12 +36,10 @@
 import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.when;
 
-import static kotlinx.coroutines.flow.StateFlowKt.MutableStateFlow;
-
 import android.app.IActivityManager;
 import android.content.pm.ActivityInfo;
 import android.content.res.Configuration;
-import android.testing.AndroidTestingRunner;
+import android.platform.test.flag.junit.FlagsParameterization;
 import android.testing.TestableLooper.RunWithLooper;
 import android.view.View;
 import android.view.WindowManager;
@@ -50,51 +47,26 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.internal.colorextraction.ColorExtractor;
-import com.android.keyguard.KeyguardSecurityModel;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.biometrics.AuthController;
-import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository;
 import com.android.systemui.colorextraction.SysuiColorExtractor;
-import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository;
-import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor;
 import com.android.systemui.communal.domain.interactor.CommunalInteractor;
-import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor;
 import com.android.systemui.dump.DumpManager;
-import com.android.systemui.flags.FakeFeatureFlagsClassic;
+import com.android.systemui.flags.SceneContainerFlagParameterizationKt;
 import com.android.systemui.keyguard.KeyguardViewMediator;
-import com.android.systemui.keyguard.data.repository.FakeCommandQueue;
-import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository;
-import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor;
-import com.android.systemui.keyguard.domain.interactor.FromPrimaryBouncerTransitionInteractor;
-import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
 import com.android.systemui.kosmos.KosmosJavaAdapter;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
-import com.android.systemui.power.domain.interactor.PowerInteractor;
 import com.android.systemui.res.R;
 import com.android.systemui.scene.FakeWindowRootViewComponent;
-import com.android.systemui.scene.data.repository.SceneContainerRepository;
-import com.android.systemui.scene.domain.interactor.SceneInteractor;
-import com.android.systemui.scene.shared.logger.SceneLogger;
 import com.android.systemui.settings.UserTracker;
-import com.android.systemui.shade.data.repository.FakeShadeRepository;
-import com.android.systemui.shade.domain.interactor.ShadeInteractor;
-import com.android.systemui.shade.domain.interactor.ShadeInteractorImpl;
-import com.android.systemui.shade.domain.interactor.ShadeInteractorLegacyImpl;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
-import com.android.systemui.statusbar.disableflags.data.repository.FakeDisableFlagsRepository;
-import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor;
 import com.android.systemui.statusbar.phone.DozeParameters;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
-import com.android.systemui.statusbar.phone.ScreenOffAnimationController;
 import com.android.systemui.statusbar.phone.ScrimController;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
-import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController;
-import com.android.systemui.statusbar.policy.data.repository.FakeUserSetupRepository;
 import com.android.systemui.user.domain.interactor.SelectedUserInteractor;
-import com.android.systemui.user.domain.interactor.UserSwitcherInteractor;
 
 import com.google.common.util.concurrent.MoreExecutors;
 
@@ -110,13 +82,13 @@
 import java.util.List;
 import java.util.concurrent.Executor;
 
-import kotlinx.coroutines.test.TestScope;
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
+import platform.test.runner.parameterized.Parameters;
 
-@RunWith(AndroidTestingRunner.class)
+@RunWith(ParameterizedAndroidJunit4.class)
 @RunWithLooper(setAsMainLooper = true)
 @SmallTest
 public class NotificationShadeWindowControllerImplTest extends SysuiTestCase {
-
     @Mock private WindowManager mWindowManager;
     @Mock private DozeParameters mDozeParameters;
     @Spy private final NotificationShadeWindowView mNotificationShadeWindowView = spy(
@@ -128,29 +100,31 @@
     @Mock private SysuiColorExtractor mColorExtractor;
     @Mock private ColorExtractor.GradientColors mGradientColors;
     @Mock private DumpManager mDumpManager;
-    @Mock private KeyguardSecurityModel mKeyguardSecurityModel;
     @Mock private KeyguardStateController mKeyguardStateController;
     @Mock private AuthController mAuthController;
     @Mock private ShadeWindowLogger mShadeWindowLogger;
     @Mock private SelectedUserInteractor mSelectedUserInteractor;
     @Mock private UserTracker mUserTracker;
-    @Mock private LargeScreenHeaderHelper mLargeScreenHeaderHelper;
     @Captor private ArgumentCaptor<WindowManager.LayoutParams> mLayoutParameters;
     @Captor private ArgumentCaptor<StatusBarStateController.StateListener> mStateListener;
 
     private final Executor mMainExecutor = MoreExecutors.directExecutor();
     private final Executor mBackgroundExecutor = MoreExecutors.directExecutor();
     private final KosmosJavaAdapter mKosmos = new KosmosJavaAdapter(this);
-    private final TestScope mTestScope = mKosmos.getTestScope();
-    private ShadeInteractor mShadeInteractor;
 
     private NotificationShadeWindowControllerImpl mNotificationShadeWindowController;
     private float mPreferredRefreshRate = -1;
-    private FromLockscreenTransitionInteractor mFromLockscreenTransitionInteractor;
-    private FromPrimaryBouncerTransitionInteractor mFromPrimaryBouncerTransitionInteractor;
-    private ScreenOffAnimationController mScreenOffAnimationController;
     private SysuiStatusBarStateController mStatusBarStateController;
 
+    @Parameters(name = "{0}")
+    public static List<FlagsParameterization> getParams() {
+        return SceneContainerFlagParameterizationKt.parameterizeSceneContainerFlag();
+    }
+
+    public NotificationShadeWindowControllerImplTest(FlagsParameterization flags) {
+        mSetFlagsRule.setFlagsParameterization(flags);
+    }
+
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
@@ -164,71 +138,7 @@
         when(mDozeParameters.getAlwaysOn()).thenReturn(true);
         when(mColorExtractor.getNeutralColors()).thenReturn(mGradientColors);
 
-        FakeKeyguardRepository keyguardRepository = new FakeKeyguardRepository();
-        FakeFeatureFlagsClassic featureFlags = new FakeFeatureFlagsClassic();
-        FakeShadeRepository shadeRepository = new FakeShadeRepository();
-
-        mScreenOffAnimationController = mKosmos.getScreenOffAnimationController();
         mStatusBarStateController = spy(mKosmos.getStatusBarStateController());
-        PowerInteractor powerInteractor = mKosmos.getPowerInteractor();
-
-        SceneInteractor sceneInteractor = new SceneInteractor(
-                mTestScope.getBackgroundScope(),
-                new SceneContainerRepository(
-                        mTestScope.getBackgroundScope(),
-                        mKosmos.getFakeSceneContainerConfig(),
-                        mKosmos.getSceneDataSource()),
-                mock(SceneLogger.class),
-                mKosmos.getDeviceUnlockedInteractor());
-
-        FakeConfigurationRepository configurationRepository = new FakeConfigurationRepository();
-        KeyguardTransitionInteractor keyguardTransitionInteractor =
-                mKosmos.getKeyguardTransitionInteractor();
-        KeyguardInteractor keyguardInteractor = new KeyguardInteractor(
-                keyguardRepository,
-                new FakeCommandQueue(),
-                powerInteractor,
-                new FakeKeyguardBouncerRepository(),
-                new ConfigurationInteractor(configurationRepository),
-                shadeRepository,
-                keyguardTransitionInteractor,
-                () -> sceneInteractor,
-                () -> mKosmos.getFromGoneTransitionInteractor(),
-                () -> mKosmos.getSharedNotificationContainerInteractor(),
-                mTestScope);
-        CommunalInteractor communalInteractor = mKosmos.getCommunalInteractor();
-
-        mFromLockscreenTransitionInteractor = mKosmos.getFromLockscreenTransitionInteractor();
-        mFromPrimaryBouncerTransitionInteractor =
-                mKosmos.getFromPrimaryBouncerTransitionInteractor();
-
-        DeviceEntryUdfpsInteractor deviceEntryUdfpsInteractor =
-                mock(DeviceEntryUdfpsInteractor.class);
-        when(deviceEntryUdfpsInteractor.isUdfpsSupported()).thenReturn(MutableStateFlow(false));
-
-        mShadeInteractor = new ShadeInteractorImpl(
-                mTestScope.getBackgroundScope(),
-                mKosmos.getDeviceProvisioningInteractor(),
-                new FakeDisableFlagsRepository(),
-                mock(DozeParameters.class),
-                keyguardRepository,
-                keyguardTransitionInteractor,
-                powerInteractor,
-                new FakeUserSetupRepository(),
-                mock(UserSwitcherInteractor.class),
-                new ShadeInteractorLegacyImpl(
-                        mTestScope.getBackgroundScope(),
-                        keyguardRepository,
-                        new SharedNotificationContainerInteractor(
-                                configurationRepository,
-                                mContext,
-                                new ResourcesSplitShadeStateController(),
-                                keyguardInteractor,
-                                deviceEntryUdfpsInteractor,
-                                () -> mLargeScreenHeaderHelper),
-                        shadeRepository
-                )
-        );
 
         mNotificationShadeWindowController = new NotificationShadeWindowControllerImpl(
                 mContext,
@@ -245,13 +155,13 @@
                 mColorExtractor,
                 mDumpManager,
                 mKeyguardStateController,
-                mScreenOffAnimationController,
+                mKosmos.getScreenOffAnimationController(),
                 mAuthController,
-                () -> mShadeInteractor,
+                mKosmos::getShadeInteractor,
                 mShadeWindowLogger,
                 () -> mSelectedUserInteractor,
                 mUserTracker,
-                () -> communalInteractor) {
+                mKosmos::getCommunalInteractor) {
                     @Override
                     protected boolean isDebuggable() {
                         return false;
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt
index 96b2b7a..aa0ca18 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt
@@ -24,7 +24,7 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
 import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.flags.andSceneContainer
+import com.android.systemui.flags.parameterizeSceneContainerFlag
 import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
 import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.shared.model.DozeStateModel
@@ -80,7 +80,7 @@
         @JvmStatic
         @Parameters(name = "{0}")
         fun getParams(): List<FlagsParameterization> {
-            return FlagsParameterization.allCombinationsOf().andSceneContainer()
+            return parameterizeSceneContainerFlag()
         }
     }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/startable/ShadeStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/startable/ShadeStartableTest.kt
index 2ab934c..07c4b00 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/startable/ShadeStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/startable/ShadeStartableTest.kt
@@ -16,7 +16,7 @@
 
 package com.android.systemui.shade.domain.startable
 
-import androidx.test.ext.junit.runners.AndroidJUnit4
+import android.platform.test.flag.junit.FlagsParameterization
 import androidx.test.filters.SmallTest
 import com.android.compose.animation.scene.ObservableTransitionState
 import com.android.compose.animation.scene.SceneKey
@@ -25,9 +25,8 @@
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
 import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository
-import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor
 import com.android.systemui.flags.EnableSceneContainer
+import com.android.systemui.flags.parameterizeSceneContainerFlag
 import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
 import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
 import com.android.systemui.kosmos.testScope
@@ -50,24 +49,42 @@
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
-@RunWith(AndroidJUnit4::class)
-class ShadeStartableTest : SysuiTestCase() {
+@RunWith(ParameterizedAndroidJunit4::class)
+class ShadeStartableTest(flags: FlagsParameterization?) : SysuiTestCase() {
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
-    private val shadeInteractor = kosmos.shadeInteractor
-    private val sceneInteractor = kosmos.sceneInteractor
-    private val shadeExpansionStateManager = kosmos.shadeExpansionStateManager
-    private val deviceEntryRepository = kosmos.fakeDeviceEntryRepository
-    private val deviceUnlockedInteractor = kosmos.deviceUnlockedInteractor
-    private val fakeConfigurationRepository = kosmos.fakeConfigurationRepository
-    private val fakeSceneDataSource = kosmos.fakeSceneDataSource
+    private val shadeInteractor by lazy { kosmos.shadeInteractor }
+    private val sceneInteractor by lazy { kosmos.sceneInteractor }
+    private val shadeExpansionStateManager by lazy { kosmos.shadeExpansionStateManager }
+    private val fakeConfigurationRepository by lazy { kosmos.fakeConfigurationRepository }
+    private val fakeSceneDataSource by lazy { kosmos.fakeSceneDataSource }
 
-    private val underTest = kosmos.shadeStartable
+    private lateinit var underTest: ShadeStartable
+
+    companion object {
+        @JvmStatic
+        @Parameters(name = "{0}")
+        fun getParams(): List<FlagsParameterization> {
+            return parameterizeSceneContainerFlag()
+        }
+    }
+
+    init {
+        mSetFlagsRule.setFlagsParameterization(flags!!)
+    }
+
+    @Before
+    fun setup() {
+        underTest = kosmos.shadeStartable
+    }
 
     @Test
     fun hydrateShadeMode() =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
index 2397de6..5312ad8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
@@ -28,6 +28,7 @@
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository
 import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
+import com.android.systemui.flags.EnableSceneContainer
 import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
 import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
 import com.android.systemui.kosmos.testScope
@@ -65,6 +66,7 @@
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 @TestableLooper.RunWithLooper
+@EnableSceneContainer
 class ShadeSceneViewModelTest : SysuiTestCase() {
 
     private val kosmos = testKosmos()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
similarity index 69%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
index be5af88..fc9535c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2020 The Android Open Source Project
+ * 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.
@@ -19,60 +19,32 @@
 package com.android.systemui.statusbar
 
 import android.animation.ObjectAnimator
-import android.testing.AndroidTestingRunner
+import android.platform.test.flag.junit.FlagsParameterization
 import android.testing.TestableLooper
 import androidx.test.filters.SmallTest
 import com.android.internal.logging.testing.UiEventLoggerFake
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
-import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository
-import com.android.systemui.classifier.FalsingCollectorFake
-import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository
-import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
 import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor
 import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor
 import com.android.systemui.flags.DisableSceneContainer
 import com.android.systemui.flags.EnableSceneContainer
-import com.android.systemui.flags.FakeFeatureFlagsClassic
+import com.android.systemui.flags.parameterizeSceneContainerFlag
 import com.android.systemui.jank.interactionJankMonitor
-import com.android.systemui.keyguard.data.repository.FakeCommandQueue
-import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
-import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
-import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor
-import com.android.systemui.keyguard.domain.interactor.FromPrimaryBouncerTransitionInteractor
-import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
-import com.android.systemui.keyguard.domain.interactor.fromGoneTransitionInteractor
-import com.android.systemui.keyguard.domain.interactor.fromLockscreenTransitionInteractor
-import com.android.systemui.keyguard.domain.interactor.fromPrimaryBouncerTransitionInteractor
 import com.android.systemui.keyguard.domain.interactor.keyguardClockInteractor
-import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.plugins.statusbar.StatusBarStateController
-import com.android.systemui.power.data.repository.FakePowerRepository
-import com.android.systemui.power.domain.interactor.PowerInteractor
 import com.android.systemui.scene.domain.interactor.sceneInteractor
 import com.android.systemui.scene.shared.model.Scenes
-import com.android.systemui.shade.LargeScreenHeaderHelper
-import com.android.systemui.shade.data.repository.FakeShadeRepository
-import com.android.systemui.shade.domain.interactor.ShadeInteractor
-import com.android.systemui.shade.domain.interactor.ShadeInteractorImpl
-import com.android.systemui.shade.domain.interactor.ShadeInteractorLegacyImpl
-import com.android.systemui.statusbar.disableflags.data.repository.FakeDisableFlagsRepository
-import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor
-import com.android.systemui.statusbar.notification.stack.domain.interactor.sharedNotificationContainerInteractor
-import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController
-import com.android.systemui.statusbar.policy.data.repository.FakeUserSetupRepository
-import com.android.systemui.statusbar.policy.domain.interactor.deviceProvisioningInteractor
+import com.android.systemui.shade.domain.interactor.shadeInteractor
 import com.android.systemui.testKosmos
 import com.android.systemui.util.kotlin.JavaAdapter
 import com.android.systemui.util.mockito.mock
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Assert.assertEquals
@@ -87,26 +59,34 @@
 import org.mockito.Mockito.mock
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
-import org.mockito.Mockito.`when` as whenever
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
 
 @SmallTest
-@RunWith(AndroidTestingRunner::class)
+@RunWith(ParameterizedAndroidJunit4::class)
 @TestableLooper.RunWithLooper
-class StatusBarStateControllerImplTest : SysuiTestCase() {
+class StatusBarStateControllerImplTest(flags: FlagsParameterization?) : SysuiTestCase() {
 
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
-    private lateinit var shadeInteractor: ShadeInteractor
-    private lateinit var fromLockscreenTransitionInteractor: FromLockscreenTransitionInteractor
-    private lateinit var fromPrimaryBouncerTransitionInteractor:
-        FromPrimaryBouncerTransitionInteractor
+
     private val mockDarkAnimator = mock<ObjectAnimator>()
-    private val deviceEntryUdfpsInteractor = mock<DeviceEntryUdfpsInteractor>()
-    private val largeScreenHeaderHelper = mock<LargeScreenHeaderHelper>()
 
     private lateinit var underTest: StatusBarStateControllerImpl
     private lateinit var uiEventLogger: UiEventLoggerFake
 
+    companion object {
+        @JvmStatic
+        @Parameters(name = "{0}")
+        fun getParams(): List<FlagsParameterization> {
+            return parameterizeSceneContainerFlag()
+        }
+    }
+
+    init {
+        mSetFlagsRule.setFlagsParameterization(flags!!)
+    }
+
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
@@ -118,7 +98,7 @@
                     uiEventLogger,
                     kosmos.interactionJankMonitor,
                     JavaAdapter(testScope.backgroundScope),
-                    { shadeInteractor },
+                    { kosmos.shadeInteractor },
                     { kosmos.deviceUnlockedInteractor },
                     { kosmos.sceneInteractor },
                     { kosmos.keyguardClockInteractor },
@@ -127,59 +107,6 @@
                     return mockDarkAnimator
                 }
             }
-
-        val powerInteractor =
-            PowerInteractor(FakePowerRepository(), FalsingCollectorFake(), mock(), underTest)
-        val keyguardRepository = FakeKeyguardRepository()
-        val keyguardTransitionRepository = FakeKeyguardTransitionRepository()
-        val featureFlags = FakeFeatureFlagsClassic()
-        val shadeRepository = FakeShadeRepository()
-        val configurationRepository = FakeConfigurationRepository()
-        val keyguardTransitionInteractor = kosmos.keyguardTransitionInteractor
-        fromLockscreenTransitionInteractor = kosmos.fromLockscreenTransitionInteractor
-        fromPrimaryBouncerTransitionInteractor = kosmos.fromPrimaryBouncerTransitionInteractor
-
-        val keyguardInteractor =
-            KeyguardInteractor(
-                keyguardRepository,
-                FakeCommandQueue(),
-                powerInteractor,
-                FakeKeyguardBouncerRepository(),
-                ConfigurationInteractor(configurationRepository),
-                shadeRepository,
-                keyguardTransitionInteractor,
-                { kosmos.sceneInteractor },
-                { kosmos.fromGoneTransitionInteractor },
-                { kosmos.sharedNotificationContainerInteractor },
-                testScope,
-            )
-
-        whenever(deviceEntryUdfpsInteractor.isUdfpsSupported).thenReturn(MutableStateFlow(false))
-        shadeInteractor =
-            ShadeInteractorImpl(
-                testScope.backgroundScope,
-                kosmos.deviceProvisioningInteractor,
-                FakeDisableFlagsRepository(),
-                mock(),
-                keyguardRepository,
-                keyguardTransitionInteractor,
-                powerInteractor,
-                FakeUserSetupRepository(),
-                mock(),
-                ShadeInteractorLegacyImpl(
-                    testScope.backgroundScope,
-                    keyguardRepository,
-                    SharedNotificationContainerInteractor(
-                        configurationRepository,
-                        mContext,
-                        ResourcesSplitShadeStateController(),
-                        keyguardInteractor,
-                        deviceEntryUdfpsInteractor,
-                        largeScreenHeaderHelperLazy = { largeScreenHeaderHelper }
-                    ),
-                    shadeRepository,
-                )
-            )
     }
 
     @Test
diff --git a/packages/SystemUI/res/anim/shortcut_helper_close_anim.xml b/packages/SystemUI/res/anim/shortcut_helper_close_anim.xml
new file mode 100644
index 0000000..47fd78a
--- /dev/null
+++ b/packages/SystemUI/res/anim/shortcut_helper_close_anim.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.
+  -->
+
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+    android:interpolator="@android:anim/accelerate_interpolator"
+    android:zAdjustment="top">
+
+    <translate
+        android:fromYDelta="0"
+        android:toYDelta="100%"
+        android:duration="@android:integer/config_shortAnimTime" />
+</set>
diff --git a/packages/SystemUI/res/anim/shortcut_helper_launch_anim.xml b/packages/SystemUI/res/anim/shortcut_helper_launch_anim.xml
new file mode 100644
index 0000000..77edf58
--- /dev/null
+++ b/packages/SystemUI/res/anim/shortcut_helper_launch_anim.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<!-- Animation for when a dock window at the bottom of the screen is entering. -->
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+    android:interpolator="@android:anim/accelerate_decelerate_interpolator"
+    android:zAdjustment="top">
+
+    <translate android:fromYDelta="100%"
+        android:toYDelta="0"
+        android:startOffset="@android:integer/config_shortAnimTime"
+        android:duration="@android:integer/config_mediumAnimTime"/>
+</set>
diff --git a/packages/SystemUI/res/layout/activity_keyboard_shortcut_helper.xml b/packages/SystemUI/res/layout/activity_keyboard_shortcut_helper.xml
new file mode 100644
index 0000000..292e496
--- /dev/null
+++ b/packages/SystemUI/res/layout/activity_keyboard_shortcut_helper.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/shortcut_helper_sheet_container"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <LinearLayout
+        android:id="@+id/shortcut_helper_sheet"
+        style="@style/Widget.Material3.BottomSheet"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="vertical"
+        app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
+
+        <!-- Drag handle for accessibility -->
+        <com.google.android.material.bottomsheet.BottomSheetDragHandleView
+            android:id="@+id/drag_handle"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content" />
+
+        <TextView
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            android:layout_weight="1"
+            android:gravity="center"
+            android:textAppearance="?textAppearanceDisplayLarge"
+            android:background="?colorTertiaryContainer"
+            android:text="Shortcut Helper Content" />
+    </LinearLayout>
+</androidx.coordinatorlayout.widget.CoordinatorLayout>
diff --git a/packages/SystemUI/res/values-night/styles.xml b/packages/SystemUI/res/values-night/styles.xml
index 291f8a2..546bf1c 100644
--- a/packages/SystemUI/res/values-night/styles.xml
+++ b/packages/SystemUI/res/values-night/styles.xml
@@ -72,4 +72,8 @@
         <item name="android:textColor">@color/material_dynamic_secondary80</item>
     </style>
 
+    <style name="ShortcutHelperTheme" parent="@style/ShortcutHelperThemeCommon">
+        <item name="android:windowLightNavigationBar">false</item>
+    </style>
+
 </resources>
diff --git a/packages/SystemUI/res/values-xlarge-land/config.xml b/packages/SystemUI/res/values-xlarge-land/config.xml
new file mode 100644
index 0000000..5e4304e
--- /dev/null
+++ b/packages/SystemUI/res/values-xlarge-land/config.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<resources>
+    <item name="shortcut_helper_screen_width_fraction"  format="float" type="dimen">0.8</item>
+</resources>
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 4a73d85..19273ec 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -1010,4 +1010,7 @@
 
     <!-- Whether volume panel should use the large screen layout or not -->
     <bool name="volume_panel_is_large_screen">false</bool>
+
+    <!-- The width of the shortcut helper container, as a fraction of the screen's width. -->
+    <item name="shortcut_helper_screen_width_fraction" format="float" type="dimen">1.0</item>
 </resources>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 9aff9fd..b8e78a4 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -1718,6 +1718,11 @@
     <!-- Accessibility label for available satellite connection [CHAR LIMIT=NONE] -->
     <string name="accessibility_status_bar_satellite_available">Satellite, connection available</string>
 
+    <!-- Text displayed indicating that the user is connected to a satellite signal. -->
+    <string name="satellite_connected_carrier_text">Connected to satellite</string>
+    <!-- Text displayed indicating that the user is not connected to a satellite signal. -->
+    <string name="satellite_not_connected_carrier_text">Not connected to satellite</string>
+
     <!-- Accessibility label for managed profile icon (not shown on screen) [CHAR LIMIT=NONE] -->
     <string name="accessibility_managed_profile">Work profile</string>
 
@@ -1991,7 +1996,7 @@
     <string name="keyboard_shortcut_clear_text">Clear search query</string>
     <!-- The title for keyboard shortcut search list [CHAR LIMIT=25] -->
     <string name="keyboard_shortcut_search_list_title">Keyboard Shortcuts</string>
-    <!-- The hint for keyboard shortcut search list [CHAR LIMIT=25] -->
+    <!-- The hint for keyboard shortcut search list [CHAR LIMIT=50] -->
     <string name="keyboard_shortcut_search_list_hint">Search shortcuts</string>
     <!-- The description for no shortcuts results [CHAR LIMIT=25] -->
     <string name="keyboard_shortcut_search_list_no_result">No shortcuts found</string>
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index 2c9006e..3d57111 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -1654,4 +1654,28 @@
     <style name="Theme.SystemUI.Dialog.StickyKeys" parent="@style/Theme.SystemUI.Dialog">
         <item name="android:colorBackground">@color/transparent</item>
     </style>
+
+    <style name="ShortcutHelperAnimation" parent="@android:style/Animation.Activity">
+        <item name="android:activityOpenEnterAnimation">@anim/shortcut_helper_launch_anim</item>
+        <item name="android:taskOpenEnterAnimation">@anim/shortcut_helper_launch_anim</item>
+        <item name="android:activityOpenExitAnimation">@anim/shortcut_helper_close_anim</item>
+        <item name="android:taskOpenExitAnimation">@anim/shortcut_helper_close_anim</item>
+    </style>
+
+    <style name="ShortcutHelperThemeCommon" parent="@style/Theme.Material3.DynamicColors.DayNight">
+        <item name="android:windowAnimationStyle">@style/ShortcutHelperAnimation</item>
+        <item name="android:windowIsTranslucent">true</item>
+        <item name="android:windowNoTitle">true</item>
+        <item name="android:windowBackground">@android:color/transparent</item>
+        <item name="android:backgroundDimEnabled">true</item>
+        <item name="android:statusBarColor">@android:color/transparent</item>
+        <item name="android:windowContentOverlay">@null</item>
+        <item name="android:navigationBarColor">@android:color/transparent</item>
+        <item name="android:windowLayoutInDisplayCutoutMode">always</item>
+        <item name="enableEdgeToEdge">true</item>
+    </style>
+
+    <style name="ShortcutHelperTheme" parent="@style/ShortcutHelperThemeCommon">
+        <item name="android:windowLightNavigationBar">true</item>
+    </style>
 </resources>
diff --git a/packages/SystemUI/src/com/android/keyguard/CarrierTextManager.java b/packages/SystemUI/src/com/android/keyguard/CarrierTextManager.java
index b9b8fbe..5647b0b 100644
--- a/packages/SystemUI/src/com/android/keyguard/CarrierTextManager.java
+++ b/packages/SystemUI/src/com/android/keyguard/CarrierTextManager.java
@@ -19,6 +19,7 @@
 import static com.android.keyguard.logging.CarrierTextManagerLogger.REASON_ACTIVE_DATA_SUB_CHANGED;
 import static com.android.keyguard.logging.CarrierTextManagerLogger.REASON_ON_TELEPHONY_CAPABLE;
 import static com.android.keyguard.logging.CarrierTextManagerLogger.REASON_REFRESH_CARRIER_INFO;
+import static com.android.keyguard.logging.CarrierTextManagerLogger.REASON_SATELLITE_CHANGED;
 import static com.android.keyguard.logging.CarrierTextManagerLogger.REASON_SIM_ERROR_STATE_CHANGED;
 
 import android.content.Context;
@@ -43,17 +44,22 @@
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
 import com.android.systemui.res.R;
+import com.android.systemui.statusbar.pipeline.satellite.ui.viewmodel.DeviceBasedSatelliteViewModel;
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository;
 import com.android.systemui.telephony.TelephonyListenerManager;
+import com.android.systemui.util.kotlin.JavaAdapter;
 
 import java.util.Arrays;
 import java.util.List;
 import java.util.Objects;
+import java.util.concurrent.CancellationException;
 import java.util.concurrent.Executor;
 import java.util.concurrent.atomic.AtomicBoolean;
 
 import javax.inject.Inject;
 
+import kotlinx.coroutines.Job;
+
 /**
  * Controller that generates text including the carrier names and/or the status of all the SIM
  * interfaces in the device. Through a callback, the updates can be retrieved either as a list or
@@ -77,10 +83,17 @@
     protected KeyguardUpdateMonitor mKeyguardUpdateMonitor;
     private final CarrierTextManagerLogger mLogger;
     private final WifiRepository mWifiRepository;
+    private final DeviceBasedSatelliteViewModel mDeviceBasedSatelliteViewModel;
+    private final JavaAdapter mJavaAdapter;
     private final boolean[] mSimErrorState;
     private final int mSimSlotsNumber;
     @Nullable // Check for nullability before dispatching
     private CarrierTextCallback mCarrierTextCallback;
+    @Nullable
+    private Job mSatelliteConnectionJob;
+
+    @Nullable private String mSatelliteCarrierText;
+
     private final Context mContext;
     private final TelephonyManager mTelephonyManager;
     private final CharSequence mSeparator;
@@ -178,6 +191,8 @@
             boolean showAirplaneMode,
             boolean showMissingSim,
             WifiRepository wifiRepository,
+            DeviceBasedSatelliteViewModel deviceBasedSatelliteViewModel,
+            JavaAdapter javaAdapter,
             TelephonyManager telephonyManager,
             TelephonyListenerManager telephonyListenerManager,
             WakefulnessLifecycle wakefulnessLifecycle,
@@ -192,6 +207,8 @@
         mShowAirplaneMode = showAirplaneMode;
         mShowMissingSim = showMissingSim;
         mWifiRepository = wifiRepository;
+        mDeviceBasedSatelliteViewModel = deviceBasedSatelliteViewModel;
+        mJavaAdapter = javaAdapter;
         mTelephonyManager = telephonyManager;
         mSeparator = separator;
         mTelephonyListenerManager = telephonyListenerManager;
@@ -282,6 +299,11 @@
                     mWakefulnessLifecycle.addObserver(mWakefulnessObserver);
                 });
                 mTelephonyListenerManager.addActiveDataSubscriptionIdListener(mPhoneStateListener);
+                cancelSatelliteCollectionJob(/* reason= */ "Starting new job");
+                mSatelliteConnectionJob =
+                    mJavaAdapter.alwaysCollectFlow(
+                        mDeviceBasedSatelliteViewModel.getCarrierText(),
+                        this::onSatelliteCarrierTextChanged);
             } else {
                 // Don't listen and clear out the text when the device isn't a phone.
                 mMainExecutor.execute(() -> callback.updateCarrierInfo(
@@ -294,6 +316,7 @@
                 mWakefulnessLifecycle.removeObserver(mWakefulnessObserver);
             });
             mTelephonyListenerManager.removeActiveDataSubscriptionIdListener(mPhoneStateListener);
+            cancelSatelliteCollectionJob(/* reason= */ "Stopping listening");
         }
     }
 
@@ -311,6 +334,12 @@
         return mKeyguardUpdateMonitor.getFilteredSubscriptionInfo();
     }
 
+    private void onSatelliteCarrierTextChanged(@Nullable String text) {
+        mLogger.logUpdateCarrierTextForReason(REASON_SATELLITE_CHANGED);
+        mSatelliteCarrierText = text;
+        updateCarrierText();
+    }
+
     protected void updateCarrierText() {
         Trace.beginSection("CarrierTextManager#updateCarrierText");
         boolean allSimsMissing = true;
@@ -411,6 +440,12 @@
             airplaneMode = true;
         }
 
+        String currentSatelliteText = mSatelliteCarrierText;
+        if (currentSatelliteText != null) {
+            mLogger.logUsingSatelliteText(currentSatelliteText);
+            displayText = currentSatelliteText;
+        }
+
         final CarrierTextCallbackInfo info = new CarrierTextCallbackInfo(
                 displayText,
                 carrierNames,
@@ -616,11 +651,20 @@
         return list;
     }
 
+    private void cancelSatelliteCollectionJob(String reason) {
+        Job job = mSatelliteConnectionJob;
+        if (job != null) {
+            job.cancel(new CancellationException(reason));
+        }
+    }
+
     /** Injectable Buildeer for {@#link CarrierTextManager}. */
     public static class Builder {
         private final Context mContext;
         private final String mSeparator;
         private final WifiRepository mWifiRepository;
+        private final DeviceBasedSatelliteViewModel mDeviceBasedSatelliteViewModel;
+        private final JavaAdapter mJavaAdapter;
         private final TelephonyManager mTelephonyManager;
         private final TelephonyListenerManager mTelephonyListenerManager;
         private final WakefulnessLifecycle mWakefulnessLifecycle;
@@ -637,6 +681,8 @@
                 Context context,
                 @Main Resources resources,
                 @Nullable WifiRepository wifiRepository,
+                DeviceBasedSatelliteViewModel deviceBasedSatelliteViewModel,
+                JavaAdapter javaAdapter,
                 TelephonyManager telephonyManager,
                 TelephonyListenerManager telephonyListenerManager,
                 WakefulnessLifecycle wakefulnessLifecycle,
@@ -648,6 +694,8 @@
             mSeparator = resources.getString(
                     com.android.internal.R.string.kg_text_message_separator);
             mWifiRepository = wifiRepository;
+            mDeviceBasedSatelliteViewModel = deviceBasedSatelliteViewModel;
+            mJavaAdapter = javaAdapter;
             mTelephonyManager = telephonyManager;
             mTelephonyListenerManager = telephonyListenerManager;
             mWakefulnessLifecycle = wakefulnessLifecycle;
@@ -682,9 +730,20 @@
         public CarrierTextManager build() {
             mLogger.setLocation(mDebugLocation);
             return new CarrierTextManager(
-                    mContext, mSeparator, mShowAirplaneMode, mShowMissingSim, mWifiRepository,
-                    mTelephonyManager, mTelephonyListenerManager, mWakefulnessLifecycle,
-                    mMainExecutor, mBgExecutor, mKeyguardUpdateMonitor, mLogger);
+                    mContext,
+                    mSeparator,
+                    mShowAirplaneMode,
+                    mShowMissingSim,
+                    mWifiRepository,
+                    mDeviceBasedSatelliteViewModel,
+                    mJavaAdapter,
+                    mTelephonyManager,
+                    mTelephonyListenerManager,
+                    mWakefulnessLifecycle,
+                    mMainExecutor,
+                    mBgExecutor,
+                    mKeyguardUpdateMonitor,
+                    mLogger);
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index 8c51a4e..519622e 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -42,7 +42,6 @@
 import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_LOCKOUT;
 import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN;
 import static com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_OPENED;
-import static com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_UNKNOWN;
 
 import android.annotation.AnyThread;
 import android.annotation.MainThread;
@@ -1868,7 +1867,9 @@
                 @Override
                 public void onPostureChanged(@DevicePostureInt int posture) {
                     if (posture == DEVICE_POSTURE_OPENED) {
-                        mLogger.d("Posture changed to open - attempting to request active unlock");
+                        mLogger.d("Posture changed to open - attempting to request active"
+                                + " unlock and run face auth");
+                        getFaceAuthInteractor().onDeviceUnfolded();
                         requestActiveUnlockFromWakeReason(PowerManager.WAKE_REASON_UNFOLD_DEVICE,
                                 false);
                     }
@@ -2434,9 +2435,7 @@
                 updateFingerprintListeningState(BIOMETRIC_ACTION_START);
             }
         });
-        if (mConfigFaceAuthSupportedPosture != DEVICE_POSTURE_UNKNOWN) {
-            mDevicePostureController.addCallback(mPostureCallback);
-        }
+        mDevicePostureController.addCallback(mPostureCallback);
         updateFingerprintListeningState(BIOMETRIC_ACTION_UPDATE);
 
         mTaskStackChangeListeners.registerTaskStackListener(mTaskStackListener);
diff --git a/packages/SystemUI/src/com/android/keyguard/logging/CarrierTextManagerLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/CarrierTextManagerLogger.kt
index cb474d3..48fea55 100644
--- a/packages/SystemUI/src/com/android/keyguard/logging/CarrierTextManagerLogger.kt
+++ b/packages/SystemUI/src/com/android/keyguard/logging/CarrierTextManagerLogger.kt
@@ -77,6 +77,15 @@
         )
     }
 
+    fun logUsingSatelliteText(satelliteText: String) {
+        buffer.log(
+            TAG,
+            LogLevel.VERBOSE,
+            { str1 = satelliteText },
+            { "┣ updateCarrierText: using satellite text. text=$str1" },
+        )
+    }
+
     /** De-structures the info object so that we don't have to generate new strings */
     fun logCallbackSentFromUpdate(info: CarrierTextCallbackInfo) {
         buffer.log(
@@ -129,6 +138,7 @@
         const val REASON_ON_TELEPHONY_CAPABLE = 2
         const val REASON_SIM_ERROR_STATE_CHANGED = 3
         const val REASON_ACTIVE_DATA_SUB_CHANGED = 4
+        const val REASON_SATELLITE_CHANGED = 5
 
         @Retention(AnnotationRetention.SOURCE)
         @IntDef(
@@ -138,6 +148,7 @@
                     REASON_ON_TELEPHONY_CAPABLE,
                     REASON_SIM_ERROR_STATE_CHANGED,
                     REASON_ACTIVE_DATA_SUB_CHANGED,
+                    REASON_SATELLITE_CHANGED,
                 ]
         )
         annotation class CarrierTextRefreshReason
@@ -148,6 +159,7 @@
                 REASON_ON_TELEPHONY_CAPABLE -> "ON_TELEPHONY_CAPABLE"
                 REASON_SIM_ERROR_STATE_CHANGED -> "SIM_ERROR_STATE_CHANGED"
                 REASON_ACTIVE_DATA_SUB_CHANGED -> "ACTIVE_DATA_SUB_CHANGED"
+                REASON_SATELLITE_CHANGED -> "SATELLITE_CHANGED"
                 else -> "unknown"
             }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/CoreStartable.java b/packages/SystemUI/src/com/android/systemui/CoreStartable.java
index 39e1c41..55ccaa6 100644
--- a/packages/SystemUI/src/com/android/systemui/CoreStartable.java
+++ b/packages/SystemUI/src/com/android/systemui/CoreStartable.java
@@ -33,12 +33,23 @@
  *  abstract fun bind(impl: FoobarStartable): CoreStartable
  *  </pre>
  *
- * If your CoreStartable depends on different CoreStartables starting before it, use a
- * {@link com.android.systemui.startable.Dependencies} annotation to list out those dependencies.
+ * If your CoreStartable depends on different CoreStartables starting before it, you can specify
+ * another map binding listing out its dependencies:
+ *  <pre>
+ *  &#64;Provides
+ *  &#64;IntoMap
+ *  &#64;Dependencies  // Important! com.android.systemui.startable.Dependencies.
+ *  &#64;ClassKey(FoobarStartable::class)
+ *  fun providesDeps(): Set&lt;Class&lt;out CoreStartable&gt;&gt; {
+ *      return setOf(OtherStartable::class.java)
+ *  }
+ *  </pre>
+ *
  *
  * @see SystemUIApplication#startSystemUserServicesIfNeeded()
  */
 public interface CoreStartable extends Dumpable {
+    String STARTABLE_DEPENDENCIES = "startable_dependencies";
 
     /** Main entry point for implementations. Called shortly after SysUI startup. */
     void start();
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
index ba869bd..4b07402 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
@@ -44,16 +44,15 @@
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.process.ProcessWrapper;
 import com.android.systemui.res.R;
-import com.android.systemui.startable.Dependencies;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.util.NotificationChannels;
 
 import java.lang.reflect.InvocationTargetException;
 import java.util.ArrayDeque;
-import java.util.Arrays;
 import java.util.Comparator;
 import java.util.HashSet;
 import java.util.Map;
+import java.util.Set;
 import java.util.StringJoiner;
 import java.util.TreeMap;
 
@@ -300,6 +299,7 @@
         int serviceIndex = 0;
 
         do {
+            startedAny = false;
             queue = nextQueue;
             nextQueue = new ArrayDeque<>(startables.size());
 
@@ -307,9 +307,9 @@
                 Map.Entry<Class<?>, Provider<CoreStartable>> entry = queue.removeFirst();
 
                 Class<?> cls = entry.getKey();
-                Dependencies dep = cls.getAnnotation(Dependencies.class);
-                Class<?>[] deps = (dep == null ? null : dep.value());
-                if (deps == null || startedStartables.containsAll(Arrays.asList(deps))) {
+                Set<Class<? extends CoreStartable>> deps =
+                        mSysUIComponent.getStartableDependencies().get(cls);
+                if (deps == null || startedStartables.containsAll(deps)) {
                     String clsName = cls.getName();
                     int i = serviceIndex;  // Copied to make lambda happy.
                     timeInitialization(
@@ -331,12 +331,12 @@
             while (!nextQueue.isEmpty()) {
                 Map.Entry<Class<?>, Provider<CoreStartable>> entry = nextQueue.removeFirst();
                 Class<?> cls = entry.getKey();
-                Dependencies dep = cls.getAnnotation(Dependencies.class);
-                Class<?>[] deps = (dep == null ? null : dep.value());
+                Set<Class<? extends CoreStartable>> deps =
+                        mSysUIComponent.getStartableDependencies().get(cls);
                 StringJoiner stringJoiner = new StringJoiner(", ");
-                for (int i = 0; deps != null && i < deps.length; i++) {
-                    if (!startedStartables.contains(deps[i])) {
-                        stringJoiner.add(deps[i].getName());
+                for (Class<? extends CoreStartable> c : deps) {
+                    if (!startedStartables.contains(c)) {
+                        stringJoiner.add(c.getName());
                     }
                 }
                 Log.e(TAG, "Failed to start " + cls.getName()
diff --git a/packages/SystemUI/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandler.java b/packages/SystemUI/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandler.java
index d0f08f5..85aeb27 100644
--- a/packages/SystemUI/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandler.java
@@ -27,6 +27,7 @@
 import android.view.MotionEvent;
 import android.view.VelocityTracker;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.VisibleForTesting;
 
 import com.android.internal.logging.UiEvent;
@@ -94,13 +95,11 @@
     private Boolean mCapture;
     private Boolean mExpanded;
 
-    private boolean mBouncerInitiallyShowing;
-
     private TouchSession mTouchSession;
 
-    private ValueAnimatorCreator mValueAnimatorCreator;
+    private final ValueAnimatorCreator mValueAnimatorCreator;
 
-    private VelocityTrackerFactory mVelocityTrackerFactory;
+    private final VelocityTrackerFactory mVelocityTrackerFactory;
 
     private final UiEventLogger mUiEventLogger;
 
@@ -118,17 +117,12 @@
     private final GestureDetector.OnGestureListener mOnGestureListener =
             new GestureDetector.SimpleOnGestureListener() {
                 @Override
-                public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
+                public boolean onScroll(MotionEvent e1, @NonNull MotionEvent e2, float distanceX,
                         float distanceY) {
                     if (mCapture == null) {
-                        mBouncerInitiallyShowing = mCentralSurfaces
-                                .map(CentralSurfaces::isBouncerShowing)
-                                .orElse(false);
-
                         if (Flags.dreamOverlayBouncerSwipeDirectionFiltering()) {
                             mCapture = Math.abs(distanceY) > Math.abs(distanceX)
-                                    && ((distanceY < 0 && mBouncerInitiallyShowing)
-                                    || (distanceY > 0 && !mBouncerInitiallyShowing));
+                                    && distanceY > 0;
                         } else {
                             // If the user scrolling favors a vertical direction, begin capturing
                             // scrolls.
@@ -146,13 +140,8 @@
                         return false;
                     }
 
-                    // Don't set expansion for downward scroll when the bouncer is hidden.
-                    if (!mBouncerInitiallyShowing && (e1.getY() < e2.getY())) {
-                        return true;
-                    }
-
-                    // Don't set expansion for upward scroll when the bouncer is shown.
-                    if (mBouncerInitiallyShowing && (e1.getY() > e2.getY())) {
+                    // Don't set expansion for downward scroll.
+                    if (e1.getY() < e2.getY()) {
                         return true;
                     }
 
@@ -176,8 +165,7 @@
                     final float dragDownAmount = e2.getY() - e1.getY();
                     final float screenTravelPercentage = Math.abs(e1.getY() - e2.getY())
                             / mTouchSession.getBounds().height();
-                    setPanelExpansion(mBouncerInitiallyShowing
-                            ? screenTravelPercentage : 1 - screenTravelPercentage);
+                    setPanelExpansion(1 - screenTravelPercentage);
                     return true;
                 }
             };
@@ -223,9 +211,9 @@
             LockPatternUtils lockPatternUtils,
             UserTracker userTracker,
             @Named(BouncerSwipeModule.SWIPE_TO_BOUNCER_FLING_ANIMATION_UTILS_OPENING)
-                    FlingAnimationUtils flingAnimationUtils,
+            FlingAnimationUtils flingAnimationUtils,
             @Named(BouncerSwipeModule.SWIPE_TO_BOUNCER_FLING_ANIMATION_UTILS_CLOSING)
-                    FlingAnimationUtils flingAnimationUtilsClosing,
+            FlingAnimationUtils flingAnimationUtilsClosing,
             @Named(BouncerSwipeModule.SWIPE_TO_BOUNCER_START_REGION) float swipeRegionPercentage,
             @Named(BouncerSwipeModule.MIN_BOUNCER_ZONE_SCREEN_PERCENTAGE) float minRegionPercentage,
             UiEventLogger uiEventLogger) {
@@ -247,17 +235,13 @@
     public void getTouchInitiationRegion(Rect bounds, Region region, Rect exclusionRect) {
         final int width = bounds.width();
         final int height = bounds.height();
-        final float minBouncerHeight = height * mMinBouncerZoneScreenPercentage;
         final int minAllowableBottom = Math.round(height * (1 - mMinBouncerZoneScreenPercentage));
 
-        final boolean isBouncerShowing =
-                mCentralSurfaces.map(CentralSurfaces::isBouncerShowing).orElse(false);
-        final Rect normalRegion = isBouncerShowing
-                ? new Rect(0, 0, width, Math.round(height * mBouncerZoneScreenPercentage))
-                : new Rect(0, Math.round(height * (1 - mBouncerZoneScreenPercentage)),
-                        width, height);
+        final Rect normalRegion = new Rect(0,
+                Math.round(height * (1 - mBouncerZoneScreenPercentage)),
+                width, height);
 
-        if (!isBouncerShowing && exclusionRect != null) {
+        if (exclusionRect != null) {
             int lowestBottom = Math.min(Math.max(0, exclusionRect.bottom), minAllowableBottom);
             normalRegion.top = Math.max(normalRegion.top, lowestBottom);
         }
@@ -322,8 +306,7 @@
                         : KeyguardBouncerConstants.EXPANSION_HIDDEN;
 
                 // Log the swiping up to show Bouncer event.
-                if (!mBouncerInitiallyShowing
-                        && expansion == KeyguardBouncerConstants.EXPANSION_VISIBLE) {
+                if (expansion == KeyguardBouncerConstants.EXPANSION_VISIBLE) {
                     mUiEventLogger.log(DreamEvent.DREAM_SWIPED);
                 }
 
@@ -335,17 +318,15 @@
         }
     }
 
-    private ValueAnimator createExpansionAnimator(float targetExpansion, float expansionHeight) {
+    private ValueAnimator createExpansionAnimator(float targetExpansion) {
         final ValueAnimator animator =
                 mValueAnimatorCreator.create(mCurrentExpansion, targetExpansion);
         animator.addUpdateListener(
                 animation -> {
                     float expansionFraction = (float) animation.getAnimatedValue();
-                    float dragDownAmount = expansionFraction * expansionHeight;
                     setPanelExpansion(expansionFraction);
                 });
-        if (!mBouncerInitiallyShowing
-                && targetExpansion == KeyguardBouncerConstants.EXPANSION_VISIBLE) {
+        if (targetExpansion == KeyguardBouncerConstants.EXPANSION_VISIBLE) {
             animator.addListener(
                     new AnimatorListenerAdapter() {
                         @Override
@@ -381,8 +362,7 @@
         final float viewHeight = mTouchSession.getBounds().height();
         final float currentHeight = viewHeight * mCurrentExpansion;
         final float targetHeight = viewHeight * expansion;
-        final float expansionHeight = targetHeight - currentHeight;
-        final ValueAnimator animator = createExpansionAnimator(expansion, expansionHeight);
+        final ValueAnimator animator = createExpansionAnimator(expansion);
         if (expansion == KeyguardBouncerConstants.EXPANSION_HIDDEN) {
             // Hides the bouncer, i.e., fully expands the space above the bouncer.
             mFlingAnimationUtilsClosing.apply(animator, currentHeight, targetHeight, velocity,
diff --git a/packages/SystemUI/src/com/android/systemui/ambient/touch/ShadeTouchHandler.java b/packages/SystemUI/src/com/android/systemui/ambient/touch/ShadeTouchHandler.java
index 9ef9938..9c7fc9d 100644
--- a/packages/SystemUI/src/com/android/systemui/ambient/touch/ShadeTouchHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/ambient/touch/ShadeTouchHandler.java
@@ -23,7 +23,8 @@
 import android.view.GestureDetector;
 import android.view.MotionEvent;
 
-import com.android.systemui.shade.ShadeViewController;
+import androidx.annotation.NonNull;
+
 import com.android.systemui.statusbar.phone.CentralSurfaces;
 
 import java.util.Optional;
@@ -37,29 +38,34 @@
  */
 public class ShadeTouchHandler implements TouchHandler {
     private final Optional<CentralSurfaces> mSurfaces;
-    private final ShadeViewController mShadeViewController;
     private final int mInitiationHeight;
 
+    /**
+     * Tracks whether or not we are capturing a given touch. Will be null before and after a touch.
+     */
+    private Boolean mCapture;
+
     @Inject
     ShadeTouchHandler(Optional<CentralSurfaces> centralSurfaces,
-            ShadeViewController shadeViewController,
             @Named(NOTIFICATION_SHADE_GESTURE_INITIATION_HEIGHT) int initiationHeight) {
         mSurfaces = centralSurfaces;
-        mShadeViewController = shadeViewController;
         mInitiationHeight = initiationHeight;
     }
 
     @Override
     public void onSessionStart(TouchSession session) {
-        if (mSurfaces.map(CentralSurfaces::isBouncerShowing).orElse(false)) {
+        if (mSurfaces.isEmpty()) {
             session.pop();
             return;
         }
 
-        session.registerInputListener(ev -> {
-            mShadeViewController.handleExternalTouch((MotionEvent) ev);
+        session.registerCallback(() -> mCapture = null);
 
+        session.registerInputListener(ev -> {
             if (ev instanceof MotionEvent) {
+                if (mCapture != null && mCapture) {
+                    mSurfaces.get().handleExternalShadeWindowTouch((MotionEvent) ev);
+                }
                 if (((MotionEvent) ev).getAction() == MotionEvent.ACTION_UP) {
                     session.pop();
                 }
@@ -68,15 +74,25 @@
 
         session.registerGestureListener(new GestureDetector.SimpleOnGestureListener() {
             @Override
-            public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
+            public boolean onScroll(MotionEvent e1, @NonNull MotionEvent e2, float distanceX,
                     float distanceY) {
-                return true;
+                if (mCapture == null) {
+                    // Only capture swipes that are going downwards.
+                    mCapture = Math.abs(distanceY) > Math.abs(distanceX) && distanceY < 0;
+                    if (mCapture) {
+                        // Send the initial touches over, as the input listener has already
+                        // processed these touches.
+                        mSurfaces.get().handleExternalShadeWindowTouch(e1);
+                        mSurfaces.get().handleExternalShadeWindowTouch(e2);
+                    }
+                }
+                return mCapture;
             }
 
             @Override
-            public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
+            public boolean onFling(MotionEvent e1, @NonNull MotionEvent e2, float velocityX,
                     float velocityY) {
-                return true;
+                return mCapture;
             }
         });
     }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FacePropertyRepository.kt b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FacePropertyRepository.kt
index 59b59bf..7d4ba84 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FacePropertyRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FacePropertyRepository.kt
@@ -39,6 +39,7 @@
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.keyguard.shared.model.DevicePosture
 import com.android.systemui.res.R
 import java.util.concurrent.Executor
 import javax.inject.Inject
@@ -68,6 +69,8 @@
 
     /** The info of current available camera. */
     val cameraInfo: StateFlow<CameraInfo?>
+
+    val supportedPostures: List<DevicePosture>
 }
 
 /** Describes a biometric sensor */
@@ -188,6 +191,15 @@
                 initialValue = if (cameraInfoList.isNotEmpty()) cameraInfoList[0] else null
             )
 
+    private val supportedPosture =
+        applicationContext.resources.getInteger(R.integer.config_face_auth_supported_posture)
+    override val supportedPostures: List<DevicePosture> =
+        if (supportedPosture == 0) {
+            DevicePosture.entries
+        } else {
+            listOf(DevicePosture.toPosture(supportedPosture))
+        }
+
     private val defaultSensorLocation: StateFlow<Point?> =
         cameraInfo
             .map { it?.cameraLocation }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt
index 5a174b9..f437032 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.communal
 
 import android.provider.Settings
+import android.service.dreams.Flags.dreamTracksFocus
 import com.android.compose.animation.scene.SceneKey
 import com.android.systemui.CoreStartable
 import com.android.systemui.communal.domain.interactor.CommunalInteractor
@@ -25,11 +26,13 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.dock.DockManager
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.statusbar.NotificationShadeWindowController
 import com.android.systemui.util.kotlin.emitOnStart
 import com.android.systemui.util.kotlin.sample
 import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
@@ -37,15 +40,18 @@
 import javax.inject.Inject
 import kotlin.time.Duration.Companion.milliseconds
 import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.collectLatest
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.filterNotNull
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.mapLatest
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
 
 /**
  * A [CoreStartable] responsible for automatically navigating between communal scenes when certain
@@ -61,8 +67,10 @@
     private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
     private val keyguardInteractor: KeyguardInteractor,
     private val systemSettings: SystemSettings,
+    private val notificationShadeWindowController: NotificationShadeWindowController,
     @Application private val applicationScope: CoroutineScope,
     @Background private val bgScope: CoroutineScope,
+    @Main private val mainDispatcher: CoroutineDispatcher,
 ) : CoreStartable {
     private var screenTimeout: Int = DEFAULT_SCREEN_TIMEOUT
 
@@ -134,6 +142,16 @@
                     }
                 }
         }
+
+        if (dreamTracksFocus()) {
+            bgScope.launch {
+                communalInteractor.isIdleOnCommunal.collectLatest {
+                    withContext(mainDispatcher) {
+                        notificationShadeWindowController.setGlanceableHubShowing(it)
+                    }
+                }
+            }
+        }
     }
 
     private suspend fun determineSceneAfterTransition(
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
index 804c8c4..1dd3722 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
@@ -25,6 +25,7 @@
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.keyguard.KeyguardSliceProvider;
 import com.android.systemui.people.PeopleProvider;
+import com.android.systemui.startable.Dependencies;
 import com.android.systemui.statusbar.NotificationInsetsModule;
 import com.android.systemui.statusbar.QsFrameTranslateModule;
 import com.android.systemui.statusbar.policy.ConfigurationController;
@@ -47,6 +48,7 @@
 
 import java.util.Map;
 import java.util.Optional;
+import java.util.Set;
 
 import javax.inject.Provider;
 
@@ -160,6 +162,11 @@
     @PerUser Map<Class<?>, Provider<CoreStartable>> getPerUserStartables();
 
     /**
+     * Returns {@link CoreStartable} dependencies if there are any.
+     */
+    @Dependencies Map<Class<?>, Set<Class<? extends CoreStartable>>> getStartableDependencies();
+
+    /**
      * Member injection into the supplied argument.
      */
     void inject(SystemUIAppComponentFactoryBase factory);
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index 18498cb..3462164 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -30,6 +30,7 @@
 import com.android.systemui.BootCompleteCache;
 import com.android.systemui.BootCompleteCacheImpl;
 import com.android.systemui.CameraProtectionModule;
+import com.android.systemui.CoreStartable;
 import com.android.systemui.SystemUISecondaryUserService;
 import com.android.systemui.accessibility.AccessibilityModule;
 import com.android.systemui.accessibility.data.repository.AccessibilityRepositoryModule;
@@ -106,6 +107,7 @@
 import com.android.systemui.shade.transition.LargeScreenShadeInterpolatorImpl;
 import com.android.systemui.shared.condition.Monitor;
 import com.android.systemui.smartspace.dagger.SmartspaceModule;
+import com.android.systemui.startable.Dependencies;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
@@ -163,11 +165,14 @@
 import dagger.Provides;
 import dagger.multibindings.ClassKey;
 import dagger.multibindings.IntoMap;
+import dagger.multibindings.Multibinds;
 
 import kotlinx.coroutines.CoroutineScope;
 
 import java.util.Collections;
+import java.util.Map;
 import java.util.Optional;
+import java.util.Set;
 import java.util.concurrent.Executor;
 
 import javax.inject.Named;
@@ -271,6 +276,10 @@
         })
 public abstract class SystemUIModule {
 
+    @Multibinds
+    @Dependencies
+    abstract Map<Class<?>, Set<Class<? extends CoreStartable>>> startableDependencyMap();
+
     @Binds
     abstract BootCompleteCache bindBootCompleteCache(BootCompleteCacheImpl bootCompleteCache);
 
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractor.kt
index 7733de4..a32b2aa 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractor.kt
@@ -68,6 +68,7 @@
     fun onPrimaryBouncerUserInput()
     fun onAccessibilityAction()
     fun onWalletLaunched()
+    fun onDeviceUnfolded()
 
     /** Whether face auth is considered class 3 */
     fun isFaceAuthStrong(): Boolean
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/NoopDeviceEntryFaceAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/NoopDeviceEntryFaceAuthInteractor.kt
index 65f3eb7..6629f6e 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/NoopDeviceEntryFaceAuthInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/NoopDeviceEntryFaceAuthInteractor.kt
@@ -66,4 +66,5 @@
     override fun onPrimaryBouncerUserInput() {}
     override fun onAccessibilityAction() {}
     override fun onWalletLaunched() = Unit
+    override fun onDeviceUnfolded() {}
 }
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt
index 03819ed..6c6683a 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt
@@ -37,6 +37,7 @@
 import com.android.systemui.deviceentry.shared.model.FaceAuthenticationStatus
 import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.DevicePosture
 import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
 import com.android.systemui.keyguard.shared.model.KeyguardState.DOZING
 import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
@@ -248,6 +249,12 @@
         }
     }
 
+    override fun onDeviceUnfolded() {
+        if (facePropertyRepository.supportedPostures.contains(DevicePosture.OPENED)) {
+            runFaceAuth(FaceAuthUiEvent.FACE_AUTH_UPDATED_POSTURE_CHANGED, true)
+        }
+    }
+
     override fun registerListener(listener: FaceAuthenticationListener) {
         listeners.add(listener)
     }
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java
index 5a036b1..0c2709e 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java
@@ -22,6 +22,7 @@
 import static com.android.systemui.complication.ComplicationLayoutParams.POSITION_BOTTOM;
 import static com.android.systemui.complication.ComplicationLayoutParams.POSITION_TOP;
 import static com.android.systemui.doze.util.BurnInHelperKt.getBurnInOffset;
+import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow;
 
 import android.animation.Animator;
 import android.content.res.Resources;
@@ -40,6 +41,8 @@
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dreams.dagger.DreamOverlayComponent;
 import com.android.systemui.dreams.dagger.DreamOverlayModule;
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
+import com.android.systemui.keyguard.shared.model.KeyguardState;
 import com.android.systemui.res.R;
 import com.android.systemui.shade.ShadeExpansionChangeEvent;
 import com.android.systemui.statusbar.BlurUtils;
@@ -50,6 +53,8 @@
 import javax.inject.Inject;
 import javax.inject.Named;
 
+import kotlinx.coroutines.CoroutineDispatcher;
+
 /**
  * View controller for {@link DreamOverlayContainerView}.
  */
@@ -62,6 +67,7 @@
     private final DreamOverlayAnimationsController mDreamOverlayAnimationsController;
     private final DreamOverlayStateController mStateController;
     private final LowLightTransitionCoordinator mLowLightTransitionCoordinator;
+    private final KeyguardTransitionInteractor mKeyguardTransitionInteractor;
 
     private final ComplicationHostViewController mComplicationHostViewController;
 
@@ -81,6 +87,7 @@
 
     // Main thread handler used to schedule periodic tasks (e.g. burn-in protection updates).
     private final Handler mHandler;
+    private final CoroutineDispatcher mMainDispatcher;
     private final int mDreamOverlayMaxTranslationY;
     private final PrimaryBouncerCallbackInteractor mPrimaryBouncerCallbackInteractor;
 
@@ -88,6 +95,7 @@
 
     private boolean mBouncerAnimating;
     private boolean mWakingUpFromSwipe;
+    private boolean mAnyBouncerShowing;
 
     private final BouncerlessScrimController mBouncerlessScrimController;
 
@@ -170,6 +178,7 @@
             LowLightTransitionCoordinator lowLightTransitionCoordinator,
             BlurUtils blurUtils,
             @Main Handler handler,
+            @Main CoroutineDispatcher mainDispatcher,
             @Main Resources resources,
             @Named(DreamOverlayModule.MAX_BURN_IN_OFFSET) int maxBurnInOffset,
             @Named(DreamOverlayModule.BURN_IN_PROTECTION_UPDATE_INTERVAL) long
@@ -178,7 +187,8 @@
             PrimaryBouncerCallbackInteractor primaryBouncerCallbackInteractor,
             DreamOverlayAnimationsController animationsController,
             DreamOverlayStateController stateController,
-            BouncerlessScrimController bouncerlessScrimController) {
+            BouncerlessScrimController bouncerlessScrimController,
+            KeyguardTransitionInteractor keyguardTransitionInteractor) {
         super(containerView);
         mDreamOverlayContentView = contentView;
         mStatusBarViewController = statusBarViewController;
@@ -190,6 +200,8 @@
         mBouncerlessScrimController = bouncerlessScrimController;
         mBouncerlessScrimController.addCallback(mBouncerlessExpansionCallback);
 
+        mKeyguardTransitionInteractor = keyguardTransitionInteractor;
+
         mComplicationHostViewController = complicationHostViewController;
         mDreamOverlayMaxTranslationY = resources.getDimensionPixelSize(
                 R.dimen.dream_overlay_y_offset);
@@ -200,6 +212,7 @@
                         ViewGroup.LayoutParams.MATCH_PARENT));
 
         mHandler = handler;
+        mMainDispatcher = mainDispatcher;
         mMaxBurnInOffset = maxBurnInOffset;
         mBurnInProtectionUpdateInterval = burnInProtectionUpdateInterval;
         mMillisUntilFullJitter = millisUntilFullJitter;
@@ -225,6 +238,12 @@
         mView.getRootSurfaceControl().setTouchableRegion(emptyRegion);
         emptyRegion.recycle();
 
+        collectFlow(
+                mView,
+                mKeyguardTransitionInteractor.isFinishedInStateWhere(KeyguardState::isBouncerState),
+                isFinished -> mAnyBouncerShowing = isFinished,
+                mMainDispatcher);
+
         // Start dream entry animations. Skip animations for low light clock.
         if (!mStateController.isLowLightActive()) {
             // If this is transitioning from the low light dream to the user dream, the overlay
@@ -246,6 +265,10 @@
         return mView;
     }
 
+    boolean isBouncerShowing() {
+        return mAnyBouncerShowing;
+    }
+
     private void updateBurnInOffsets() {
         // Make sure the offset starts at zero, to avoid a big jump in the overlay when it first
         // appears.
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
index 1135afe..a9ef531 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
@@ -19,9 +19,11 @@
 import static com.android.systemui.dreams.dagger.DreamModule.DREAM_OVERLAY_WINDOW_TITLE;
 import static com.android.systemui.dreams.dagger.DreamModule.DREAM_TOUCH_INSET_MANAGER;
 import static com.android.systemui.dreams.dagger.DreamModule.HOME_CONTROL_PANEL_DREAM_COMPONENT;
+import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow;
 
 import android.content.ComponentName;
 import android.content.Context;
+import android.content.Intent;
 import android.graphics.drawable.ColorDrawable;
 import android.util.Log;
 import android.view.View;
@@ -34,7 +36,10 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleOwner;
 import androidx.lifecycle.LifecycleRegistry;
+import androidx.lifecycle.LifecycleService;
+import androidx.lifecycle.ServiceLifecycleDispatcher;
 import androidx.lifecycle.ViewModelStore;
 
 import com.android.dream.lowlight.dagger.LowLightDreamModule;
@@ -45,15 +50,21 @@
 import com.android.keyguard.KeyguardUpdateMonitorCallback;
 import com.android.systemui.ambient.touch.TouchMonitor;
 import com.android.systemui.ambient.touch.dagger.AmbientTouchComponent;
+import com.android.systemui.ambient.touch.scrim.ScrimManager;
+import com.android.systemui.communal.domain.interactor.CommunalInteractor;
+import com.android.systemui.communal.shared.model.CommunalScenes;
 import com.android.systemui.complication.Complication;
 import com.android.systemui.complication.dagger.ComplicationComponent;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dreams.dagger.DreamOverlayComponent;
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
+import com.android.systemui.shade.ShadeExpansionChangeEvent;
 import com.android.systemui.touch.TouchInsetManager;
 import com.android.systemui.util.concurrency.DelayableExecutor;
 
 import java.util.Arrays;
 import java.util.HashSet;
+import java.util.function.Consumer;
 
 import javax.inject.Inject;
 import javax.inject.Named;
@@ -63,7 +74,8 @@
  * dream reaches directly out to the service with a Window reference (via LayoutParams), which the
  * service uses to insert its own child Window into the dream's parent Window.
  */
-public class DreamOverlayService extends android.service.dreams.DreamOverlayService {
+public class DreamOverlayService extends android.service.dreams.DreamOverlayService implements
+        LifecycleOwner {
     private static final String TAG = "DreamOverlayService";
     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
 
@@ -76,6 +88,7 @@
     private DreamOverlayContainerViewController mDreamOverlayContainerViewController;
     private final DreamOverlayCallbackController mDreamOverlayCallbackController;
     private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
+    private final ScrimManager mScrimManager;
     @Nullable
     private final ComponentName mLowLightDreamComponent;
     @Nullable
@@ -93,6 +106,21 @@
     // True if the service has been destroyed.
     private boolean mDestroyed = false;
 
+    /**
+     * True if the notification shade is open.
+     */
+    private boolean mShadeExpanded = false;
+
+    /**
+     * True if any part of the glanceable hub is visible.
+     */
+    private boolean mCommunalVisible = false;
+
+    /**
+     * True if the primary bouncer is visible.
+     */
+    private boolean mBouncerShowing = false;
+
     private final ComplicationComponent mComplicationComponent;
 
     private final AmbientTouchComponent mAmbientTouchComponent;
@@ -102,27 +130,72 @@
 
     private final DreamOverlayComponent mDreamOverlayComponent;
 
-    private final DreamOverlayLifecycleOwner mLifecycleOwner;
+    /**
+     * This {@link LifecycleRegistry} controls when dream overlay functionality, like touch
+     * handling, should be active. It will automatically be paused when the dream overlay is hidden
+     * while dreaming, such as when the notification shade, bouncer, or glanceable hub are visible.
+     */
     private final LifecycleRegistry mLifecycleRegistry;
 
+    /**
+     * Drives the lifecycle exposed by this service's {@link #getLifecycle()}.
+     * <p>
+     * Used to mimic a {@link LifecycleService}, though we do not update the lifecycle in
+     * {@link #onBind(Intent)} since it's final in the base class.
+     */
+    private final ServiceLifecycleDispatcher mDispatcher = new ServiceLifecycleDispatcher(this);
+
     private TouchMonitor mTouchMonitor;
 
+    private final CommunalInteractor mCommunalInteractor;
+
+    private final SystemDialogsCloser mSystemDialogsCloser;
+
     private final KeyguardUpdateMonitorCallback mKeyguardCallback =
             new KeyguardUpdateMonitorCallback() {
                 @Override
                 public void onShadeExpandedChanged(boolean expanded) {
                     mExecutor.execute(() -> {
-                        if (getCurrentStateLocked() != Lifecycle.State.RESUMED
-                                && getCurrentStateLocked() != Lifecycle.State.STARTED) {
+                        if (mShadeExpanded == expanded) {
                             return;
                         }
+                        mShadeExpanded = expanded;
 
-                        setCurrentStateLocked(
-                                expanded ? Lifecycle.State.STARTED : Lifecycle.State.RESUMED);
+                        updateLifecycleStateLocked();
                     });
                 }
             };
 
+    private final Consumer<Boolean> mCommunalVisibleConsumer = new Consumer<>() {
+        @Override
+        public void accept(Boolean communalVisible) {
+            mExecutor.execute(() -> {
+                if (mCommunalVisible == communalVisible) {
+                    return;
+                }
+
+                mCommunalVisible = communalVisible;
+
+                updateLifecycleStateLocked();
+            });
+        }
+    };
+
+    private final Consumer<Boolean> mBouncerShowingConsumer = new Consumer<>() {
+        @Override
+        public void accept(Boolean bouncerShowing) {
+            mExecutor.execute(() -> {
+                if (mBouncerShowing == bouncerShowing) {
+                    return;
+                }
+
+                mBouncerShowing = bouncerShowing;
+
+                updateLifecycleStateLocked();
+            });
+        }
+    };
+
     private final DreamOverlayStateController.Callback mExitAnimationFinishedCallback =
             new DreamOverlayStateController.Callback() {
                 @Override
@@ -168,19 +241,24 @@
             AmbientTouchComponent.Factory ambientTouchComponentFactory,
             DreamOverlayStateController stateController,
             KeyguardUpdateMonitor keyguardUpdateMonitor,
+            ScrimManager scrimManager,
+            CommunalInteractor communalInteractor,
+            SystemDialogsCloser systemDialogsCloser,
             UiEventLogger uiEventLogger,
             @Named(DREAM_TOUCH_INSET_MANAGER) TouchInsetManager touchInsetManager,
             @Nullable @Named(LowLightDreamModule.LOW_LIGHT_DREAM_COMPONENT)
-                    ComponentName lowLightDreamComponent,
+            ComponentName lowLightDreamComponent,
             @Nullable @Named(HOME_CONTROL_PANEL_DREAM_COMPONENT)
-                    ComponentName homeControlPanelDreamComponent,
+            ComponentName homeControlPanelDreamComponent,
             DreamOverlayCallbackController dreamOverlayCallbackController,
+            KeyguardInteractor keyguardInteractor,
             @Named(DREAM_OVERLAY_WINDOW_TITLE) String windowTitle) {
         super(executor);
         mContext = context;
         mExecutor = executor;
         mWindowManager = windowManager;
         mKeyguardUpdateMonitor = keyguardUpdateMonitor;
+        mScrimManager = scrimManager;
         mLowLightDreamComponent = lowLightDreamComponent;
         mHomeControlPanelDreamComponent = homeControlPanelDreamComponent;
         mKeyguardUpdateMonitor.registerCallback(mKeyguardCallback);
@@ -188,6 +266,8 @@
         mUiEventLogger = uiEventLogger;
         mDreamOverlayCallbackController = dreamOverlayCallbackController;
         mWindowTitle = windowTitle;
+        mCommunalInteractor = communalInteractor;
+        mSystemDialogsCloser = systemDialogsCloser;
 
         final ViewModelStore viewModelStore = new ViewModelStore();
         final Complication.Host host =
@@ -203,10 +283,32 @@
                 new HashSet<>(Arrays.asList(
                         mDreamComplicationComponent.getHideComplicationTouchHandler(),
                         mDreamOverlayComponent.getCommunalTouchHandler())));
-        mLifecycleOwner = lifecycleOwner;
-        mLifecycleRegistry = mLifecycleOwner.getRegistry();
+        mLifecycleRegistry = lifecycleOwner.getRegistry();
 
-        mExecutor.execute(() -> setCurrentStateLocked(Lifecycle.State.CREATED));
+        mExecutor.execute(() -> setLifecycleStateLocked(Lifecycle.State.CREATED));
+
+        collectFlow(getLifecycle(), communalInteractor.isCommunalVisible(),
+                mCommunalVisibleConsumer);
+        collectFlow(getLifecycle(), keyguardInteractor.primaryBouncerShowing,
+                mBouncerShowingConsumer);
+    }
+
+    @NonNull
+    @Override
+    public Lifecycle getLifecycle() {
+        return mDispatcher.getLifecycle();
+    }
+
+    @Override
+    public void onCreate() {
+        mDispatcher.onServicePreSuperOnCreate();
+        super.onCreate();
+    }
+
+    @Override
+    public void onStart(Intent intent, int startId) {
+        mDispatcher.onServicePreSuperOnStart();
+        super.onStart(intent, startId);
     }
 
     @Override
@@ -214,19 +316,20 @@
         mKeyguardUpdateMonitor.removeCallback(mKeyguardCallback);
 
         mExecutor.execute(() -> {
-            setCurrentStateLocked(Lifecycle.State.DESTROYED);
+            setLifecycleStateLocked(Lifecycle.State.DESTROYED);
 
             resetCurrentDreamOverlayLocked();
 
             mDestroyed = true;
         });
 
+        mDispatcher.onServicePreSuperOnDestroy();
         super.onDestroy();
     }
 
     @Override
     public void onStartDream(@NonNull WindowManager.LayoutParams layoutParams) {
-        setCurrentStateLocked(Lifecycle.State.STARTED);
+        setLifecycleStateLocked(Lifecycle.State.STARTED);
 
         mUiEventLogger.log(DreamOverlayEvent.DREAM_OVERLAY_ENTER_START);
 
@@ -256,7 +359,7 @@
             return;
         }
 
-        setCurrentStateLocked(Lifecycle.State.RESUMED);
+        setLifecycleStateLocked(Lifecycle.State.RESUMED);
         mStateController.setOverlayActive(true);
         final ComponentName dreamComponent = getDreamComponent();
         mStateController.setLowLightActive(
@@ -276,14 +379,27 @@
         resetCurrentDreamOverlayLocked();
     }
 
-    private Lifecycle.State getCurrentStateLocked() {
+    private Lifecycle.State getLifecycleStateLocked() {
         return mLifecycleRegistry.getCurrentState();
     }
 
-    private void setCurrentStateLocked(Lifecycle.State state) {
+    private void setLifecycleStateLocked(Lifecycle.State state) {
         mLifecycleRegistry.setCurrentState(state);
     }
 
+    private void updateLifecycleStateLocked() {
+        if (getLifecycleStateLocked() != Lifecycle.State.RESUMED
+                && getLifecycleStateLocked() != Lifecycle.State.STARTED) {
+            return;
+        }
+
+        // If anything is on top of the dream, we should stop touch handling.
+        boolean shouldPause = mShadeExpanded || mCommunalVisible || mBouncerShowing;
+
+        setLifecycleStateLocked(
+                shouldPause ? Lifecycle.State.STARTED : Lifecycle.State.RESUMED);
+    }
+
     @Override
     public void onWakeUp() {
         if (mDreamOverlayContainerViewController != null) {
@@ -292,6 +408,28 @@
         }
     }
 
+    @Override
+    public void onComeToFront() {
+        // Make sure the bouncer is closed. Expanding the shade effectively contracts the bouncer
+        // an equal amount.
+        if (mDreamOverlayContainerViewController != null
+                && mDreamOverlayContainerViewController.isBouncerShowing()) {
+            mScrimManager.getCurrentController().expand(
+                    new ShadeExpansionChangeEvent(
+                            /* fraction= */ 1.f,
+                            /* expanded= */ false,
+                            /* tracking= */ true));
+        }
+
+        // closeSystemDialogs takes care of closing anything that responds to the
+        // {@link Intent.ACTION_CLOSE_SYSTEM_DIALOGS} broadcast (which includes the notification
+        // shade).
+        mSystemDialogsCloser.closeSystemDialogs();
+
+        // Hide glanceable hub (this is a nop if glanceable hub is not open).
+        mCommunalInteractor.changeScene(CommunalScenes.Blank, null);
+    }
+
     /**
      * Inserts {@link Window} to host the dream overlay into the dream's parent window. Must be
      * called from the main executing thread. The window attributes closely mirror those that are
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/SystemDialogsCloser.java b/packages/SystemUI/src/com/android/systemui/dreams/SystemDialogsCloser.java
new file mode 100644
index 0000000..6e7239a4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/SystemDialogsCloser.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.dreams;
+
+/** Defines an interface for a class that is responsible for closing system dialogs. */
+public interface SystemDialogsCloser {
+    /** Close any open system dialogs. */
+    void closeSystemDialogs();
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java
index 31710ac..516b8c5 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java
@@ -31,6 +31,7 @@
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dreams.DreamOverlayNotificationCountProvider;
 import com.android.systemui.dreams.DreamOverlayService;
+import com.android.systemui.dreams.SystemDialogsCloser;
 import com.android.systemui.dreams.complication.dagger.ComplicationComponent;
 import com.android.systemui.dreams.homecontrols.DreamActivityProvider;
 import com.android.systemui.dreams.homecontrols.DreamActivityProviderImpl;
@@ -146,6 +147,15 @@
         return Optional.empty();
     }
 
+    /**
+     * Provides an implementation for {@link SystemDialogsCloser} that calls
+     * {@link Context.closeSystemDialogs}.
+     */
+    @Provides
+    static SystemDialogsCloser providesSystemDialogsCloser(Context context) {
+        return () -> context.closeSystemDialogs();
+    }
+
     /** */
     @Provides
     @Named(DREAM_ONLY_ENABLED_FOR_DOCK_USER)
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..fff0c58 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.handleExternalShadeWindowTouch((MotionEvent) ev);
             if (ev != null && ((MotionEvent) ev).getAction() == MotionEvent.ACTION_UP) {
                 var unused = session.pop();
             }
diff --git a/packages/SystemUI/src/com/android/systemui/haptics/slider/HapticSliderViewBinder.kt b/packages/SystemUI/src/com/android/systemui/haptics/slider/HapticSliderViewBinder.kt
index 304fdd6..ca6c8da 100644
--- a/packages/SystemUI/src/com/android/systemui/haptics/slider/HapticSliderViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/haptics/slider/HapticSliderViewBinder.kt
@@ -23,11 +23,11 @@
 
 object HapticSliderViewBinder {
     /**
-     * Binds a [SeekableSliderHapticPlugin] to a [View]. The binded view should be a
+     * Binds a [SeekbarHapticPlugin] to a [View]. The binded view should be a
      * [android.widget.SeekBar] or a container of a [android.widget.SeekBar]
      */
     @JvmStatic
-    fun bind(view: View?, plugin: SeekableSliderHapticPlugin) {
+    fun bind(view: View?, plugin: SeekbarHapticPlugin) {
         view?.repeatWhenAttached {
             plugin.startInScope(lifecycleScope)
             try {
diff --git a/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderEventProducer.kt b/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderEventProducer.kt
deleted file mode 100644
index cfa5294..0000000
--- a/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderEventProducer.kt
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.haptics.slider
-
-import android.widget.SeekBar
-import android.widget.SeekBar.OnSeekBarChangeListener
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.update
-
-/** An event producer for a Seekable element such as the Android [SeekBar] */
-class SeekableSliderEventProducer : SliderEventProducer, OnSeekBarChangeListener {
-
-    /** The current event reported by a SeekBar */
-    private val _currentEvent = MutableStateFlow(SliderEvent(SliderEventType.NOTHING, 0f))
-
-    override fun produceEvents(): Flow<SliderEvent> = _currentEvent.asStateFlow()
-
-    override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
-        val eventType =
-            if (fromUser) SliderEventType.PROGRESS_CHANGE_BY_USER
-            else SliderEventType.PROGRESS_CHANGE_BY_PROGRAM
-
-        _currentEvent.value = SliderEvent(eventType, normalizeProgress(seekBar, progress))
-    }
-
-    /**
-     * Normalize the integer progress of a SeekBar to the range from 0F to 1F.
-     *
-     * @param[seekBar] The SeekBar that reports a progress.
-     * @param[progress] The integer progress of the SeekBar within its min and max values.
-     * @return The progress in the range from 0F to 1F.
-     */
-    private fun normalizeProgress(seekBar: SeekBar, progress: Int): Float {
-        if (seekBar.max == seekBar.min) {
-            return 1.0f
-        }
-        val range = seekBar.max - seekBar.min
-        return (progress - seekBar.min) / range.toFloat()
-    }
-
-    override fun onStartTrackingTouch(seekBar: SeekBar) {
-        _currentEvent.update { previousEvent ->
-            SliderEvent(SliderEventType.STARTED_TRACKING_TOUCH, previousEvent.currentProgress)
-        }
-    }
-
-    override fun onStopTrackingTouch(seekBar: SeekBar) {
-        _currentEvent.update { previousEvent ->
-            SliderEvent(SliderEventType.STOPPED_TRACKING_TOUCH, previousEvent.currentProgress)
-        }
-    }
-
-    /** The arrow navigation that was operating the slider has stopped. */
-    fun onArrowUp() {
-        _currentEvent.update { previousEvent ->
-            SliderEvent(SliderEventType.ARROW_UP, previousEvent.currentProgress)
-        }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderHapticPlugin.kt b/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekbarHapticPlugin.kt
similarity index 72%
rename from packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderHapticPlugin.kt
rename to packages/SystemUI/src/com/android/systemui/haptics/slider/SeekbarHapticPlugin.kt
index ed82278..2007db34 100644
--- a/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderHapticPlugin.kt
+++ b/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekbarHapticPlugin.kt
@@ -30,12 +30,12 @@
 /**
  * A plugin added to a manager of a [android.widget.SeekBar] that adds dynamic haptic feedback.
  *
- * A [SeekableSliderEventProducer] is used as the producer of slider events, a
+ * A [SliderStateProducer] is used as the producer of slider events, a
  * [SliderHapticFeedbackProvider] is used as the listener of slider states to play haptic feedback
- * depending on the state, and a [SeekableSliderTracker] is used as the state machine handler that
+ * depending on the state, and a [SliderStateTracker] is used as the state machine handler that
  * tracks and manipulates the slider state.
  */
-class SeekableSliderHapticPlugin
+class SeekbarHapticPlugin
 @JvmOverloads
 constructor(
     vibratorHelper: VibratorHelper,
@@ -46,7 +46,7 @@
 
     private val velocityTracker = VelocityTracker.obtain()
 
-    private val sliderEventProducer = SeekableSliderEventProducer()
+    private val sliderEventProducer = SliderStateProducer()
 
     private val sliderHapticFeedbackProvider =
         SliderHapticFeedbackProvider(
@@ -56,7 +56,7 @@
             systemClock,
         )
 
-    private var sliderTracker: SeekableSliderTracker? = null
+    private var sliderTracker: SliderStateTracker? = null
 
     private var pluginScope: CoroutineScope? = null
 
@@ -86,7 +86,7 @@
     fun startInScope(scope: CoroutineScope) {
         if (sliderTracker != null) stop()
         sliderTracker =
-            SeekableSliderTracker(
+            SliderStateTracker(
                 sliderHapticFeedbackProvider,
                 sliderEventProducer,
                 scope,
@@ -116,28 +116,52 @@
     /** onStartTrackingTouch event from the slider's [android.widget.SeekBar] */
     fun onStartTrackingTouch(seekBar: SeekBar) {
         if (isTracking) {
-            sliderEventProducer.onStartTrackingTouch(seekBar)
+            sliderEventProducer.onStartTracking(true)
         }
     }
 
     /** onProgressChanged event from the slider's [android.widget.SeekBar] */
     fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
         if (isTracking) {
-            sliderEventProducer.onProgressChanged(seekBar, progress, fromUser)
+            if (sliderTracker?.currentState == SliderState.IDLE && !fromUser) {
+                // This case translates to the slider starting to track program changes
+                sliderEventProducer.resetWithProgress(normalizeProgress(seekBar, progress))
+                sliderEventProducer.onStartTracking(false)
+            } else {
+                sliderEventProducer.onProgressChanged(
+                    fromUser,
+                    normalizeProgress(seekBar, progress),
+                )
+            }
         }
     }
 
+    /**
+     * Normalize the integer progress of a SeekBar to the range from 0F to 1F.
+     *
+     * @param[seekBar] The SeekBar that reports a progress.
+     * @param[progress] The integer progress of the SeekBar within its min and max values.
+     * @return The progress in the range from 0F to 1F.
+     */
+    private fun normalizeProgress(seekBar: SeekBar, progress: Int): Float {
+        if (seekBar.max == seekBar.min) {
+            return 1.0f
+        }
+        val range = seekBar.max - seekBar.min
+        return (progress - seekBar.min) / range.toFloat()
+    }
+
     /** onStopTrackingTouch event from the slider's [android.widget.SeekBar] */
     fun onStopTrackingTouch(seekBar: SeekBar) {
         if (isTracking) {
-            sliderEventProducer.onStopTrackingTouch(seekBar)
+            sliderEventProducer.onStopTracking(true)
         }
     }
 
-    /** onArrowUp event recorded */
-    fun onArrowUp() {
+    /** Programmatic changes have stopped */
+    private fun onStoppedTrackingProgram() {
         if (isTracking) {
-            sliderEventProducer.onArrowUp()
+            sliderEventProducer.onStopTracking(false)
         }
     }
 
@@ -146,7 +170,7 @@
      *
      * This event is used to estimate the key-up event based on a running a timer as a waiting
      * coroutine in the [pluginScope]. A key-up event in a slider corresponds to an onArrowUp event.
-     * Therefore, [onArrowUp] must be called after the timeout.
+     * Therefore, [onStoppedTrackingProgram] must be called after the timeout.
      */
     fun onKeyDown() {
         if (!isTracking) return
@@ -158,7 +182,7 @@
         keyUpJob =
             pluginScope?.launch {
                 delay(KEY_UP_TIMEOUT)
-                onArrowUp()
+                onStoppedTrackingProgram()
             }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderEventType.kt b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderEventType.kt
index 4a63941..0edef99 100644
--- a/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderEventType.kt
+++ b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderEventType.kt
@@ -22,6 +22,8 @@
     NOTHING,
     /* The slider has captured a touch input and is tracking touch events. */
     STARTED_TRACKING_TOUCH,
+    /* The slider started tracking programmatic value changes */
+    STARTED_TRACKING_PROGRAM,
     /* The slider progress is changing due to user touch input. */
     PROGRESS_CHANGE_BY_USER,
     /* The slider progress is changing programmatically. */
@@ -29,5 +31,5 @@
     /* The slider has stopped tracking touch events. */
     STOPPED_TRACKING_TOUCH,
     /* The external (not touch) stimulus that was modifying the slider progress has stopped. */
-    ARROW_UP,
+    STOPPED_TRACKING_PROGRAM,
 }
diff --git a/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderStateProducer.kt b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderStateProducer.kt
new file mode 100644
index 0000000..1124ab1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderStateProducer.kt
@@ -0,0 +1,62 @@
+/*
+ * 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.haptics.slider
+
+import androidx.annotation.FloatRange
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+
+/** A stateful producer of [SliderEvent] */
+class SliderStateProducer : SliderEventProducer {
+
+    /** The current event of a slider */
+    private val _currentEvent = MutableStateFlow(SliderEvent(SliderEventType.NOTHING, 0f))
+
+    override fun produceEvents(): Flow<SliderEvent> = _currentEvent.asStateFlow()
+
+    fun onProgressChanged(fromUser: Boolean, @FloatRange(from = 0.0, to = 1.0) progress: Float) {
+        val eventType =
+            if (fromUser) SliderEventType.PROGRESS_CHANGE_BY_USER
+            else SliderEventType.PROGRESS_CHANGE_BY_PROGRAM
+
+        _currentEvent.value = SliderEvent(eventType, progress)
+    }
+
+    fun onStartTracking(fromUser: Boolean) {
+        val eventType =
+            if (fromUser) SliderEventType.STARTED_TRACKING_TOUCH
+            else SliderEventType.STARTED_TRACKING_PROGRAM
+        _currentEvent.update { previousEvent ->
+            SliderEvent(eventType, previousEvent.currentProgress)
+        }
+    }
+
+    fun onStopTracking(fromUser: Boolean) {
+        val eventType =
+            if (fromUser) SliderEventType.STOPPED_TRACKING_TOUCH
+            else SliderEventType.STOPPED_TRACKING_PROGRAM
+        _currentEvent.update { previousEvent ->
+            SliderEvent(eventType, previousEvent.currentProgress)
+        }
+    }
+
+    fun resetWithProgress(@FloatRange(from = 0.0, to = 1.0) progress: Float) {
+        _currentEvent.value = SliderEvent(SliderEventType.NOTHING, progress)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderTracker.kt b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderStateTracker.kt
similarity index 94%
rename from packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderTracker.kt
rename to packages/SystemUI/src/com/android/systemui/haptics/slider/SliderStateTracker.kt
index 0af3038..14cf411 100644
--- a/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderTracker.kt
+++ b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderStateTracker.kt
@@ -25,11 +25,12 @@
 import kotlinx.coroutines.launch
 
 /**
- * Slider tracker attached to a seekable slider.
+ * Slider tracker attached to a slider.
  *
- * The tracker runs a state machine to execute actions on touch-based events typical of a seekable
- * slider such as [android.widget.SeekBar]. Coroutines responsible for running the state machine,
- * collecting slider events and maintaining waiting states are run on the provided [CoroutineScope].
+ * The tracker runs a state machine to execute actions on touch-based events typical of a general
+ * slider (including a [android.widget.SeekBar]). Coroutines responsible for running the state
+ * machine, collecting slider events and maintaining waiting states are run on the provided
+ * [CoroutineScope].
  *
  * @param[sliderStateListener] Listener of the slider state.
  * @param[sliderEventProducer] Producer of slider events arising from the slider.
@@ -37,7 +38,7 @@
  *   events and the launch of timer jobs.
  * @property[config] Configuration parameters of the slider tracker.
  */
-class SeekableSliderTracker(
+class SliderStateTracker(
     sliderStateListener: SliderStateListener,
     sliderEventProducer: SliderEventProducer,
     trackerScope: CoroutineScope,
@@ -79,7 +80,7 @@
             // This will disambiguate between an imprecise touch that acquires the slider handle,
             // and a select and jump operation in the slider track.
             setState(SliderState.WAIT)
-        } else if (newEventType == SliderEventType.PROGRESS_CHANGE_BY_PROGRAM) {
+        } else if (newEventType == SliderEventType.STARTED_TRACKING_PROGRAM) {
             val state =
                 if (bookendReached(currentProgress)) SliderState.ARROW_HANDLE_REACHED_BOOKEND
                 else SliderState.ARROW_HANDLE_MOVED_ONCE
@@ -227,7 +228,7 @@
                 }
                 SliderEventType.PROGRESS_CHANGE_BY_PROGRAM ->
                     SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY
-                SliderEventType.ARROW_UP -> SliderState.IDLE
+                SliderEventType.STOPPED_TRACKING_PROGRAM -> SliderState.IDLE
                 else -> SliderState.ARROW_HANDLE_MOVED_ONCE
             }
         setState(nextState)
@@ -237,7 +238,7 @@
         val reachedBookend = bookendReached(currentProgress)
         val nextState =
             when (newEventType) {
-                SliderEventType.ARROW_UP -> SliderState.IDLE
+                SliderEventType.STOPPED_TRACKING_PROGRAM -> SliderState.IDLE
                 SliderEventType.STARTED_TRACKING_TOUCH -> {
                     // Launching the timer and going to WAIT
                     timerJob = launchTimer()
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperActivity.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperActivity.kt
new file mode 100644
index 0000000..692fbb0
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperActivity.kt
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyboard.shortcut
+
+import android.graphics.Insets
+import android.os.Bundle
+import android.view.View
+import android.view.WindowInsets
+import androidx.activity.BackEventCompat
+import androidx.activity.ComponentActivity
+import androidx.activity.OnBackPressedCallback
+import androidx.core.view.updatePadding
+import com.android.systemui.res.R
+import com.google.android.material.bottomsheet.BottomSheetBehavior
+import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback
+import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HIDDEN
+
+/**
+ * Activity that hosts the new version of the keyboard shortcut helper. It will be used both for
+ * small and large screen devices.
+ */
+class ShortcutHelperActivity : ComponentActivity() {
+
+    private val bottomSheetContainer
+        get() = requireViewById<View>(R.id.shortcut_helper_sheet_container)
+
+    private val bottomSheet
+        get() = requireViewById<View>(R.id.shortcut_helper_sheet)
+
+    private val bottomSheetBehavior
+        get() = BottomSheetBehavior.from(bottomSheet)
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        setupEdgeToEdge()
+        super.onCreate(savedInstanceState)
+        setContentView(R.layout.activity_keyboard_shortcut_helper)
+        setUpBottomSheetWidth()
+        setUpInsets()
+        setUpPredictiveBack()
+        setUpSheetDismissListener()
+        setUpDismissOnTouchOutside()
+    }
+
+    private fun setupEdgeToEdge() {
+        // Draw behind system bars
+        window.setDecorFitsSystemWindows(false)
+    }
+
+    private fun setUpBottomSheetWidth() {
+        val sheetScreenWidthFraction =
+            resources.getFloat(R.dimen.shortcut_helper_screen_width_fraction)
+        // maxWidth needs to be set before the sheet is drawn, otherwise the call will have no
+        // effect.
+        val screenWidth = resources.displayMetrics.widthPixels
+        bottomSheetBehavior.maxWidth = (sheetScreenWidthFraction * screenWidth).toInt()
+    }
+
+    private fun setUpInsets() {
+        bottomSheetContainer.setOnApplyWindowInsetsListener { _, insets ->
+            val safeDrawingInsets = insets.safeDrawing
+            // Make sure the bottom sheet is not covered by the status bar.
+            bottomSheetContainer.updatePadding(top = safeDrawingInsets.top)
+            // Make sure the contents inside of the bottom sheet are not hidden by system bars, or
+            // cutouts.
+            bottomSheet.updatePadding(
+                left = safeDrawingInsets.left,
+                right = safeDrawingInsets.right,
+                bottom = safeDrawingInsets.bottom
+            )
+            // The bottom sheet has to be expanded only after setting up insets, otherwise there is
+            // a bug and it will not use full height.
+            expandBottomSheet()
+
+            // Return CONSUMED if you don't want want the window insets to keep passing
+            // down to descendant views.
+            WindowInsets.CONSUMED
+        }
+    }
+
+    private fun expandBottomSheet() {
+        bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
+        bottomSheetBehavior.skipCollapsed = true
+    }
+
+    private fun setUpPredictiveBack() {
+        val onBackPressedCallback =
+            object : OnBackPressedCallback(/* enabled= */ true) {
+                override fun handleOnBackStarted(backEvent: BackEventCompat) {
+                    bottomSheetBehavior.startBackProgress(backEvent)
+                }
+
+                override fun handleOnBackProgressed(backEvent: BackEventCompat) {
+                    bottomSheetBehavior.updateBackProgress(backEvent)
+                }
+
+                override fun handleOnBackPressed() {
+                    bottomSheetBehavior.handleBackInvoked()
+                }
+
+                override fun handleOnBackCancelled() {
+                    bottomSheetBehavior.cancelBackProgress()
+                }
+            }
+        onBackPressedDispatcher.addCallback(
+            owner = this,
+            onBackPressedCallback = onBackPressedCallback
+        )
+    }
+
+    private fun setUpSheetDismissListener() {
+        bottomSheetBehavior.addBottomSheetCallback(
+            object : BottomSheetCallback() {
+                override fun onStateChanged(bottomSheet: View, newState: Int) {
+                    if (newState == STATE_HIDDEN) {
+                        finish()
+                    }
+                }
+
+                override fun onSlide(bottomSheet: View, slideOffset: Float) {}
+            }
+        )
+    }
+
+    private fun setUpDismissOnTouchOutside() {
+        bottomSheetContainer.setOnClickListener { finish() }
+    }
+}
+
+private val WindowInsets.safeDrawing
+    get() =
+        getInsets(WindowInsets.Type.systemBars())
+            .union(getInsets(WindowInsets.Type.ime()))
+            .union(getInsets(WindowInsets.Type.displayCutout()))
+
+private fun Insets.union(insets: Insets): Insets = Insets.max(this, insets)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
index 7224536..d191768 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
@@ -226,7 +226,7 @@
     val ambientIndicationVisible: Flow<Boolean> = repository.ambientIndicationVisible.asStateFlow()
 
     /** Whether the primary bouncer is showing or not. */
-    val primaryBouncerShowing: Flow<Boolean> = bouncerRepository.primaryBouncerShow
+    @JvmField val primaryBouncerShowing: Flow<Boolean> = bouncerRepository.primaryBouncerShow
 
     /** Whether the alternate bouncer is showing or not. */
     val alternateBouncerShowing: Flow<Boolean> = bouncerRepository.alternateBouncerVisible
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt
index 92612b8..7d05539 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt
@@ -87,6 +87,7 @@
         }
 
         /** Whether either of the bouncers are visible when we're FINISHED in the given state. */
+        @JvmStatic
         fun isBouncerState(state: KeyguardState): Boolean {
             return state == PRIMARY_BOUNCER || state == ALTERNATE_BOUNCER
         }
diff --git a/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt b/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt
index 5dafd94..c997617 100644
--- a/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt
+++ b/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt
@@ -17,6 +17,7 @@
 
 package com.android.systemui.lifecycle
 
+import android.os.Trace
 import android.view.View
 import android.view.ViewTreeObserver
 import androidx.annotation.MainThread
@@ -73,7 +74,7 @@
         Dispatchers.Main + createCoroutineTracingContext() + coroutineContext
     val traceName =
         if (Compile.IS_DEBUG && coroutineTracing()) {
-            traceSectionName()
+            inferTraceSectionName()
         } else {
             DEFAULT_TRACE_NAME
         }
@@ -197,16 +198,21 @@
     frame.className != CURRENT_CLASS_NAME && frame.className != JAVA_ADAPTER_CLASS_NAME
 
 /** Get a name for the trace section include the name of the call site. */
-private fun traceSectionName(): String {
-    val interestingFrame =
-        StackWalker.getInstance().walk { stream ->
-            stream.filter(::isFrameInteresting).limit(5).findFirst()
+private fun inferTraceSectionName(): String {
+    try {
+        Trace.traceBegin(Trace.TRACE_TAG_APP, "RepeatWhenAttachedKt#inferTraceSectionName")
+        val interestingFrame =
+            StackWalker.getInstance().walk { stream ->
+                stream.filter(::isFrameInteresting).limit(5).findFirst()
+            }
+        if (interestingFrame.isPresent) {
+            val f = interestingFrame.get()
+            return "${f.className}#${f.methodName}:${f.lineNumber} [$DEFAULT_TRACE_NAME]"
+        } else {
+            return DEFAULT_TRACE_NAME
         }
-    if (interestingFrame.isPresent) {
-        val frame = interestingFrame.get()
-        return "${frame.className}#${frame.methodName}:${frame.lineNumber} [$DEFAULT_TRACE_NAME]"
-    } else {
-        return DEFAULT_TRACE_NAME
+    } finally {
+        Trace.traceEnd(Trace.TRACE_TAG_APP)
     }
 }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java
index 9790a52..d5b05ef 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java
@@ -1348,15 +1348,12 @@
         mDefaultDataSubId = defaultDataSubId;
     }
 
-    boolean mayLaunchShareWifiSettings(WifiEntry wifiEntry) {
+    boolean mayLaunchShareWifiSettings(WifiEntry wifiEntry, View view) {
         Intent intent = getConfiguratorQrCodeGeneratorIntentOrNull(wifiEntry);
         if (intent == null) {
             return false;
         }
-        if (mCallback != null) {
-            mCallback.dismissDialog();
-        }
-        mActivityStarter.startActivity(intent, false /* dismissShade */);
+        startActivity(intent, view);
         return true;
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegate.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegate.java
index 24089a7..1a881b6 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegate.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegate.java
@@ -395,7 +395,7 @@
                 });
         mDoneButton.setOnClickListener(v -> dialog.dismiss());
         mShareWifiButton.setOnClickListener(v -> {
-            if (mInternetDialogController.mayLaunchShareWifiSettings(mConnectedWifiEntry)) {
+            if (mInternetDialogController.mayLaunchShareWifiSettings(mConnectedWifiEntry, v)) {
                 mUiEventLogger.log(InternetDialogEvent.SHARE_WIFI_QS_BUTTON_CLICKED);
             }
         });
diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderController.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderController.java
index b425fb9..083cee7 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderController.java
+++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderController.java
@@ -33,7 +33,7 @@
 import com.android.systemui.Gefingerpoken;
 import com.android.systemui.classifier.Classifier;
 import com.android.systemui.haptics.slider.HapticSliderViewBinder;
-import com.android.systemui.haptics.slider.SeekableSliderHapticPlugin;
+import com.android.systemui.haptics.slider.SeekbarHapticPlugin;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.res.R;
@@ -65,7 +65,7 @@
     private final FalsingManager mFalsingManager;
     private final UiEventLogger mUiEventLogger;
 
-    private final SeekableSliderHapticPlugin mBrightnessSliderHapticPlugin;
+    private final SeekbarHapticPlugin mBrightnessSliderHapticPlugin;
     private final ActivityStarter mActivityStarter;
 
     private final Gefingerpoken mOnInterceptListener = new Gefingerpoken() {
@@ -89,7 +89,7 @@
             BrightnessSliderView brightnessSliderView,
             FalsingManager falsingManager,
             UiEventLogger uiEventLogger,
-            SeekableSliderHapticPlugin brightnessSliderHapticPlugin,
+            SeekbarHapticPlugin brightnessSliderHapticPlugin,
             ActivityStarter activityStarter) {
         super(brightnessSliderView);
         mFalsingManager = falsingManager;
@@ -314,7 +314,7 @@
             int layout = getLayout();
             BrightnessSliderView root = (BrightnessSliderView) LayoutInflater.from(context)
                     .inflate(layout, viewRoot, false);
-            SeekableSliderHapticPlugin plugin = new SeekableSliderHapticPlugin(
+            SeekbarHapticPlugin plugin = new SeekbarHapticPlugin(
                     mVibratorHelper,
                     mSystemClock);
             if (hapticBrightnessSlider()) {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
index a8481cd..a5a5474 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
@@ -28,10 +28,14 @@
 import androidx.activity.setViewTreeOnBackPressedDispatcherOwner
 import androidx.compose.ui.platform.ComposeView
 import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.LifecycleRegistry
 import androidx.lifecycle.lifecycleScope
 import androidx.lifecycle.repeatOnLifecycle
 import com.android.compose.theme.PlatformTheme
 import com.android.internal.annotations.VisibleForTesting
+import com.android.systemui.ambient.touch.TouchMonitor
+import com.android.systemui.ambient.touch.dagger.AmbientTouchComponent
 import com.android.systemui.communal.dagger.Communal
 import com.android.systemui.communal.domain.interactor.CommunalInteractor
 import com.android.systemui.communal.ui.compose.CommunalContainer
@@ -45,6 +49,8 @@
 import com.android.systemui.scene.shared.model.SceneDataSourceDelegator
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.statusbar.phone.SystemUIDialogFactory
+import com.android.systemui.util.kotlin.BooleanFlowOperators.and
+import com.android.systemui.util.kotlin.BooleanFlowOperators.not
 import com.android.systemui.util.kotlin.BooleanFlowOperators.or
 import com.android.systemui.util.kotlin.collectFlow
 import javax.inject.Inject
@@ -67,12 +73,27 @@
     private val shadeInteractor: ShadeInteractor,
     private val powerManager: PowerManager,
     private val communalColors: CommunalColors,
-    @Communal private val dataSourceDelegator: SceneDataSourceDelegator,
-) {
+    private val ambientTouchComponentFactory: AmbientTouchComponent.Factory,
+    @Communal private val dataSourceDelegator: SceneDataSourceDelegator
+) : LifecycleOwner {
     /** The container view for the hub. This will not be initialized until [initView] is called. */
     private var communalContainerView: View? = null
 
     /**
+     * This lifecycle is used to control when the [touchMonitor] listens to touches. The lifecycle
+     * should only be [Lifecycle.State.RESUMED] when the hub is showing and not covered by anything,
+     * such as the notification shade or bouncer.
+     */
+    private var lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this)
+
+    /**
+     * This [TouchMonitor] listens for top and bottom swipe gestures globally when the hub is open.
+     * When a top or bottom swipe is detected, they will be intercepted and used to open the
+     * notification shade/bouncer.
+     */
+    private var touchMonitor: TouchMonitor? = null
+
+    /**
      * The width of the area in which a right edge swipe can open the hub, in pixels. Read from
      * resources when [initView] is called.
      */
@@ -80,20 +101,6 @@
     private var rightEdgeSwipeRegionWidth: Int = 0
 
     /**
-     * The height of the area in which a top edge swipe while the hub is open will not intercept
-     * touches, in pixels. This allows the top edge swipe to instead open the notification shade.
-     * Read from resources when [initView] is called.
-     */
-    private var topEdgeSwipeRegionWidth: Int = 0
-
-    /**
-     * The height of the area in which a bottom edge swipe while the hub is open will not intercept
-     * touches, in pixels. This allows the bottom edge swipe to instead open the bouncer. Read from
-     * resources when [initView] is called.
-     */
-    private var bottomEdgeSwipeRegionWidth: Int = 0
-
-    /**
      * True if we are currently tracking a gesture for opening the hub that started in the edge
      * swipe region.
      */
@@ -102,9 +109,6 @@
     /** True if we are currently tracking a touch on the hub while it's open. */
     private var isTrackingHubTouch = false
 
-    /** True if we are tracking a top or bottom swipe gesture while the hub is open. */
-    private var isTrackingHubGesture = false
-
     /**
      * True if the hub UI is fully open, meaning it should receive touch input.
      *
@@ -121,9 +125,15 @@
     private var anyBouncerShowing = false
 
     /**
-     * True if the shade is fully expanded, meaning the hub should not receive any touch input.
+     * True if the shade is fully expanded and the user is not interacting with it anymore, meaning
+     * the hub should not receive any touch input.
      *
-     * Tracks [ShadeInteractor.isAnyFullyExpanded].
+     * We need to not pause the touch handling lifecycle as soon as the shade opens because if the
+     * user swipes down, then back up without lifting their finger, the lifecycle will be paused
+     * then resumed, and resuming force-stops all active touch sessions. This means the shade will
+     * not receive the end of the gesture and will be stuck open.
+     *
+     * Based on [ShadeInteractor.isAnyFullyExpanded] and [ShadeInteractor.isUserInteracting].
      */
     private var shadeShowing = false
 
@@ -132,8 +142,6 @@
      * and just let the dream overlay's touch handling deal with them.
      *
      * Tracks [KeyguardInteractor.isDreaming].
-     *
-     * TODO(b/328838259): figure out a proper solution for touch handling above the lock screen too
      */
     private var isDreaming = false
 
@@ -192,28 +200,45 @@
             throw RuntimeException("Communal view has already been initialized")
         }
 
+        if (touchMonitor == null) {
+            touchMonitor =
+                ambientTouchComponentFactory.create(this, HashSet()).getTouchMonitor().apply {
+                    init()
+                }
+        }
+        lifecycleRegistry.currentState = Lifecycle.State.CREATED
+
         communalContainerView = containerView
 
         rightEdgeSwipeRegionWidth =
             containerView.resources.getDimensionPixelSize(
                 R.dimen.communal_right_edge_swipe_region_width
             )
-        topEdgeSwipeRegionWidth =
-            containerView.resources.getDimensionPixelSize(
-                R.dimen.communal_top_edge_swipe_region_height
-            )
-        bottomEdgeSwipeRegionWidth =
-            containerView.resources.getDimensionPixelSize(
-                R.dimen.communal_bottom_edge_swipe_region_height
-            )
 
         collectFlow(
             containerView,
             keyguardTransitionInteractor.isFinishedInStateWhere(KeyguardState::isBouncerState),
-            { anyBouncerShowing = it }
+            {
+                anyBouncerShowing = it
+                updateLifecycleState()
+            }
         )
-        collectFlow(containerView, communalInteractor.isCommunalShowing, { hubShowing = it })
-        collectFlow(containerView, shadeInteractor.isAnyFullyExpanded, { shadeShowing = it })
+        collectFlow(
+            containerView,
+            communalInteractor.isCommunalShowing,
+            {
+                hubShowing = it
+                updateLifecycleState()
+            }
+        )
+        collectFlow(
+            containerView,
+            and(shadeInteractor.isAnyFullyExpanded, not(shadeInteractor.isUserInteracting)),
+            {
+                shadeShowing = it
+                updateLifecycleState()
+            }
+        )
         collectFlow(containerView, keyguardInteractor.isDreaming, { isDreaming = it })
 
         communalContainerView = containerView
@@ -221,10 +246,24 @@
         return containerView
     }
 
+    /**
+     * Updates the lifecycle stored by the [lifecycleRegistry] to control when the [touchMonitor]
+     * should listen for and intercept top and bottom swipes.
+     */
+    private fun updateLifecycleState() {
+        val shouldInterceptGestures = hubShowing && !(shadeShowing || anyBouncerShowing)
+        if (shouldInterceptGestures) {
+            lifecycleRegistry.currentState = Lifecycle.State.RESUMED
+        } else {
+            lifecycleRegistry.currentState = Lifecycle.State.STARTED
+        }
+    }
+
     /** Removes the container view from its parent. */
     fun disposeView() {
         communalContainerView?.let {
             (it.parent as ViewGroup).removeView(it)
+            lifecycleRegistry.currentState = Lifecycle.State.CREATED
             communalContainerView = null
         }
     }
@@ -262,15 +301,7 @@
         if (isDown && !hubOccluded) {
             // Only intercept down events if the hub isn't occluded by the bouncer or
             // notification shade.
-            val y = ev.rawY
-            val topSwipe: Boolean = y <= topEdgeSwipeRegionWidth
-            val bottomSwipe = y >= view.height - bottomEdgeSwipeRegionWidth
-
-            if (topSwipe || bottomSwipe) {
-                isTrackingHubGesture = true
-            } else {
-                isTrackingHubTouch = true
-            }
+            isTrackingHubTouch = true
         }
 
         if (isTrackingHubTouch) {
@@ -283,19 +314,6 @@
             // gesture
             // may return false from dispatchTouchEvent.
             return true
-        } else if (isTrackingHubGesture) {
-            // Tracking a top or bottom swipe on the hub UI.
-            if (isUp || isCancel) {
-                isTrackingHubGesture = false
-            }
-
-            // If we're dreaming, intercept touches so the hub UI doesn't receive them, but
-            // don't do anything so that the dream's touch handling takes care of opening
-            // the bouncer or shade.
-            //
-            // If we're not dreaming, we don't intercept touches at the top/bottom edge so that
-            // swipes can open the notification shade and bouncer.
-            return isDreaming
         }
 
         return false
@@ -347,4 +365,7 @@
             0
         )
     }
+
+    override val lifecycle: Lifecycle
+        get() = lifecycleRegistry
 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
index adcb14a..8b7e11c 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
@@ -448,6 +448,9 @@
                 || mScreenOffAnimationController.shouldIgnoreKeyguardTouches()) {
             mLpChanged.flags &= ~LayoutParams.FLAG_NOT_FOCUSABLE;
             mLpChanged.flags &= ~LayoutParams.FLAG_ALT_FOCUSABLE_IM;
+        } else if (state.glanceableHubShowing) {
+            mLpChanged.flags &= ~LayoutParams.FLAG_NOT_FOCUSABLE;
+            mLpChanged.flags &= ~LayoutParams.FLAG_ALT_FOCUSABLE_IM;
         } else if (state.isKeyguardShowingAndNotOccluded() || panelFocusable) {
             mLpChanged.flags &= ~LayoutParams.FLAG_NOT_FOCUSABLE;
             // Make sure to remove FLAG_ALT_FOCUSABLE_IM when keyguard needs input.
@@ -611,6 +614,7 @@
                 state.panelVisible,
                 state.shadeOrQsExpanded,
                 state.notificationShadeFocusable,
+                state.glanceableHubShowing,
                 state.bouncerShowing,
                 state.keyguardFadingAway,
                 state.keyguardGoingAway,
@@ -740,6 +744,12 @@
     }
 
     @Override
+    public void setGlanceableHubShowing(boolean showing) {
+        mCurrentState.glanceableHubShowing = showing;
+        apply(mCurrentState);
+    }
+
+    @Override
     public void setBackdropShowing(boolean showing) {
         mCurrentState.mediaBackdropShowing = showing;
         apply(mCurrentState);
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt
index e0a98b3..6a4b52a 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt
@@ -35,6 +35,7 @@
     @JvmField var shadeOrQsExpanded: Boolean = false,
     @JvmField var notificationShadeFocusable: Boolean = false,
     @JvmField var bouncerShowing: Boolean = false,
+    @JvmField var glanceableHubShowing: Boolean = false,
     @JvmField var keyguardFadingAway: Boolean = false,
     @JvmField var keyguardGoingAway: Boolean = false,
     @JvmField var qsExpanded: Boolean = false,
@@ -79,6 +80,7 @@
             shadeOrQsExpanded.toString(),
             notificationShadeFocusable.toString(),
             bouncerShowing.toString(),
+            glanceableHubShowing.toString(),
             keyguardFadingAway.toString(),
             keyguardGoingAway.toString(),
             qsExpanded.toString(),
@@ -119,6 +121,7 @@
             panelVisible: Boolean,
             panelExpanded: Boolean,
             notificationShadeFocusable: Boolean,
+            glanceableHubShowing: Boolean,
             bouncerShowing: Boolean,
             keyguardFadingAway: Boolean,
             keyguardGoingAway: Boolean,
@@ -149,6 +152,7 @@
                 this.panelVisible = panelVisible
                 this.shadeOrQsExpanded = panelExpanded
                 this.notificationShadeFocusable = notificationShadeFocusable
+                this.glanceableHubShowing = glanceableHubShowing
                 this.bouncerShowing = bouncerShowing
                 this.keyguardFadingAway = keyguardFadingAway
                 this.keyguardGoingAway = keyguardGoingAway
@@ -197,6 +201,7 @@
                 "panelVisible",
                 "panelExpanded",
                 "notificationShadeFocusable",
+                "glanceableHubShowing",
                 "bouncerShowing",
                 "keyguardFadingAway",
                 "keyguardGoingAway",
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
index 907cf5e..44f86da 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
@@ -25,7 +25,6 @@
 import android.app.StatusBarManager;
 import android.util.Log;
 import android.view.GestureDetector;
-import android.view.InputDevice;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.View;
@@ -74,14 +73,14 @@
 import com.android.systemui.unfold.UnfoldTransitionProgressProvider;
 import com.android.systemui.util.time.SystemClock;
 
+import kotlinx.coroutines.ExperimentalCoroutinesApi;
+
 import java.io.PrintWriter;
 import java.util.Optional;
 import java.util.function.Consumer;
 
 import javax.inject.Inject;
 
-import kotlinx.coroutines.ExperimentalCoroutinesApi;
-
 /**
  * Controller for {@link NotificationShadeWindowView}.
  */
@@ -137,6 +136,11 @@
     private final PanelExpansionInteractor mPanelExpansionInteractor;
     private final ShadeExpansionStateManager mShadeExpansionStateManager;
 
+    /**
+     * If {@code true}, an external touch sent in {@link #handleExternalTouch(MotionEvent)} has been
+     * intercepted and all future touch events for the gesture should be processed by this view.
+     */
+    private boolean mExternalTouchIntercepted = false;
     private boolean mIsTrackingBarGesture = false;
     private boolean mIsOcclusionTransitionRunning = false;
     private DisableSubpixelTextTransitionListener mDisableSubpixelTextTransitionListener;
@@ -253,11 +257,28 @@
     }
 
     /**
-     * Handle a touch event while dreaming by forwarding the event to the content view.
+     * Handle a touch event while dreaming or on the hub by forwarding the event to the content
+     * view.
+     * <p>
+     * Since important logic for handling touches lives in the dispatch/intercept phases, we
+     * simulate going through all of these stages before sending onTouchEvent if intercepted.
+     *
      * @param event The event to forward.
      */
-    public void handleDreamTouch(MotionEvent event) {
-        mView.dispatchTouchEvent(event);
+    public void handleExternalTouch(MotionEvent event) {
+        if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
+            mExternalTouchIntercepted = false;
+        }
+
+        if (!mView.dispatchTouchEvent(event)) {
+            return;
+        }
+        if (!mExternalTouchIntercepted) {
+            mExternalTouchIntercepted = mView.onInterceptTouchEvent(event);
+        }
+        if (mExternalTouchIntercepted) {
+            mView.onTouchEvent(event);
+        }
     }
 
     /** Inflates the {@link R.layout#status_bar_expanded} layout and sets it up. */
diff --git a/packages/SystemUI/src/com/android/systemui/startable/Dependencies.kt b/packages/SystemUI/src/com/android/systemui/startable/Dependencies.kt
index 8eed097..396c5f2 100644
--- a/packages/SystemUI/src/com/android/systemui/startable/Dependencies.kt
+++ b/packages/SystemUI/src/com/android/systemui/startable/Dependencies.kt
@@ -16,7 +16,8 @@
 package com.android.systemui.startable
 
 import com.android.systemui.CoreStartable
-import kotlin.reflect.KClass
+import java.lang.annotation.Documented
+import javax.inject.Qualifier
 
 /**
  * Allows a [CoreStartable] to declare that it must be started after its dependencies.
@@ -24,7 +25,4 @@
  * This creates a partial, topological ordering. See [com.android.systemui.SystemUIApplication] for
  * how this ordering is enforced at runtime.
  */
-@MustBeDocumented
-@Target(AnnotationTarget.CLASS)
-@Retention(AnnotationRetention.RUNTIME)
-annotation class Dependencies(vararg val value: KClass<*> = [])
+@Qualifier @Documented @Retention(AnnotationRetention.RUNTIME) annotation class Dependencies()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java
index e669556..707d59aa 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java
@@ -86,6 +86,9 @@
     /** Sets the state of whether the bouncer is showing or not. */
     default void setBouncerShowing(boolean showing) {}
 
+    /** Sets the state of whether the glanceable hub is showing or not. */
+    default void setGlanceableHubShowing(boolean showing) {}
+
     /** Sets the state of whether the backdrop is showing or not. */
     default void setBackdropShowing(boolean showing) {}
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/SysuiStatusBarStateController.java b/packages/SystemUI/src/com/android/systemui/statusbar/SysuiStatusBarStateController.java
index d2fe20d..8104755 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/SysuiStatusBarStateController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/SysuiStatusBarStateController.java
@@ -23,7 +23,6 @@
 
 import com.android.systemui.CoreStartable;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
-import com.android.systemui.startable.Dependencies;
 import com.android.systemui.statusbar.phone.CentralSurfaces;
 
 import java.lang.annotation.Retention;
@@ -31,7 +30,6 @@
 /**
  * Sends updates to {@link StateListener}s about changes to the status bar state and dozing state
  */
-@Dependencies(CentralSurfaces.class)
 public interface SysuiStatusBarStateController extends StatusBarStateController, CoreStartable {
 
     // TODO: b/115739177 (remove this explicit ordering if we can)
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 2651d2e..7d97428 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
@@ -37,6 +37,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.keyguard.AuthKeyguardMessageArea;
+import com.android.systemui.CoreStartable;
 import com.android.systemui.Dumpable;
 import com.android.systemui.animation.ActivityTransitionAnimator;
 import com.android.systemui.animation.RemoteAnimationRunnerCompat;
@@ -50,7 +51,7 @@
 import java.io.PrintWriter;
 
 /** */
-public interface CentralSurfaces extends Dumpable, LifecycleOwner {
+public interface CentralSurfaces extends Dumpable, LifecycleOwner, CoreStartable {
     boolean MULTIUSER_DEBUG = false;
     // Should match the values in PhoneWindowManager
     String SYSTEM_DIALOG_REASON_KEY = "reason";
@@ -182,6 +183,9 @@
         return contextForUser.getPackageManager();
     }
 
+    /** Default impl for CoreStartable. */
+    default void start() {}
+
     boolean updateIsKeyguard();
 
     boolean updateIsKeyguard(boolean forceStateChange);
@@ -279,11 +283,12 @@
     void awakenDreams();
 
     /**
-     * Handle a touch event while dreaming when the touch was initiated within a prescribed
-     * swipeable area. This method is provided for cases where swiping in certain areas of a dream
-     * should be handled by CentralSurfaces instead (e.g. swiping communal hub open).
+     * Handle a touch event while dreaming or on the glanceable hub when the touch was initiated
+     * within a prescribed swipeable area. This method is provided for cases where swiping in
+     * certain areas should be handled by CentralSurfaces instead (e.g. swiping hub open, opening
+     * the notification shade over dream or hub).
      */
-    void handleDreamTouch(MotionEvent event);
+    void handleExternalShadeWindowTouch(MotionEvent event);
 
     boolean isBouncerShowing();
 
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 8af7ee8..d5e66ff 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesEmptyImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesEmptyImpl.kt
@@ -79,7 +79,7 @@
     override fun updateScrimController() {}
     override fun shouldIgnoreTouch() = false
     override fun isDeviceInteractive() = false
-    override fun handleDreamTouch(event: MotionEvent?) {}
+    override fun handleExternalShadeWindowTouch(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 e9aa7aa..b2b2cea 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -2928,8 +2928,8 @@
     };
 
     @Override
-    public void handleDreamTouch(MotionEvent event) {
-        getNotificationShadeWindowViewController().handleDreamTouch(event);
+    public void handleExternalShadeWindowTouch(MotionEvent event) {
+        getNotificationShadeWindowViewController().handleExternalTouch(event);
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
index 4129b3a..b80ff38 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
@@ -42,6 +42,8 @@
 import com.android.systemui.statusbar.pipeline.mobile.util.SubscriptionManagerProxyImpl
 import com.android.systemui.statusbar.pipeline.satellite.data.DeviceBasedSatelliteRepository
 import com.android.systemui.statusbar.pipeline.satellite.data.prod.DeviceBasedSatelliteRepositoryImpl
+import com.android.systemui.statusbar.pipeline.satellite.ui.viewmodel.DeviceBasedSatelliteViewModel
+import com.android.systemui.statusbar.pipeline.satellite.ui.viewmodel.DeviceBasedSatelliteViewModelImpl
 import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository
 import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepositoryImpl
 import com.android.systemui.statusbar.pipeline.shared.ui.binder.CollapsedStatusBarViewBinder
@@ -85,6 +87,11 @@
         impl: DeviceBasedSatelliteRepositoryImpl
     ): DeviceBasedSatelliteRepository
 
+    @Binds
+    abstract fun deviceBasedSatelliteViewModel(
+        impl: DeviceBasedSatelliteViewModelImpl
+    ): DeviceBasedSatelliteViewModel
+
     @Binds abstract fun wifiRepository(impl: WifiRepositorySwitcher): WifiRepository
 
     @Binds abstract fun wifiInteractor(impl: WifiInteractorImpl): WifiInteractor
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt
index a0291b8..f2255f3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt
@@ -16,13 +16,16 @@
 
 package com.android.systemui.statusbar.pipeline.satellite.ui.viewmodel
 
+import android.content.Context
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.core.LogLevel
+import com.android.systemui.res.R
 import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepository
 import com.android.systemui.statusbar.pipeline.dagger.OemSatelliteInputLog
 import com.android.systemui.statusbar.pipeline.satellite.domain.interactor.DeviceBasedSatelliteInteractor
+import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState
 import com.android.systemui.statusbar.pipeline.satellite.ui.model.SatelliteIconModel
 import javax.inject.Inject
 import kotlin.time.Duration.Companion.seconds
@@ -42,15 +45,30 @@
  * View-Model for the device-based satellite icon. This icon will only show in the status bar if
  * satellite is available AND all other service states are considered OOS.
  */
+interface DeviceBasedSatelliteViewModel {
+    /**
+     * The satellite icon that should be displayed, or null if no satellite icon should be
+     * displayed.
+     */
+    val icon: StateFlow<Icon?>
+
+    /**
+     * The satellite-related text that should be used as the carrier text string when satellite is
+     * active, or null if the carrier text string shouldn't include any satellite information.
+     */
+    val carrierText: StateFlow<String?>
+}
+
 @OptIn(ExperimentalCoroutinesApi::class)
-class DeviceBasedSatelliteViewModel
+class DeviceBasedSatelliteViewModelImpl
 @Inject
 constructor(
+    context: Context,
     interactor: DeviceBasedSatelliteInteractor,
     @Application scope: CoroutineScope,
     airplaneModeRepository: AirplaneModeRepository,
     @OemSatelliteInputLog logBuffer: LogBuffer,
-) {
+) : DeviceBasedSatelliteViewModel {
     private val shouldShowIcon: Flow<Boolean> =
         interactor.areAllConnectionsOutOfService.flatMapLatest { allOos ->
             if (!allOos) {
@@ -87,7 +105,7 @@
             }
             .stateIn(scope, SharingStarted.WhileSubscribed(), false)
 
-    val icon: StateFlow<Icon?> =
+    override val icon: StateFlow<Icon?> =
         combine(
                 shouldActuallyShowIcon,
                 interactor.connectionState,
@@ -101,6 +119,26 @@
             }
             .stateIn(scope, SharingStarted.WhileSubscribed(), null)
 
+    override val carrierText: StateFlow<String?> =
+        combine(
+                shouldActuallyShowIcon,
+                interactor.connectionState,
+            ) { shouldShow, connectionState ->
+                if (shouldShow) {
+                    when (connectionState) {
+                        SatelliteConnectionState.Connected ->
+                            context.getString(R.string.satellite_connected_carrier_text)
+                        SatelliteConnectionState.On ->
+                            context.getString(R.string.satellite_not_connected_carrier_text)
+                        SatelliteConnectionState.Off,
+                        SatelliteConnectionState.Unknown -> null
+                    }
+                } else {
+                    null
+                }
+            }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), null)
+
     companion object {
         private const val TAG = "DeviceBasedSatelliteViewModel"
         private val DELAY_DURATION = 10.seconds
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
index 2245541..cd82e1d 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
@@ -118,7 +118,7 @@
 import com.android.systemui.Prefs;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.haptics.slider.HapticSliderViewBinder;
-import com.android.systemui.haptics.slider.SeekableSliderHapticPlugin;
+import com.android.systemui.haptics.slider.SeekbarHapticPlugin;
 import com.android.systemui.haptics.slider.SliderHapticFeedbackConfig;
 import com.android.systemui.media.dialog.MediaOutputDialogManager;
 import com.android.systemui.plugins.VolumeDialog;
@@ -2640,7 +2640,7 @@
         private ObjectAnimator anim;  // slider progress animation for non-touch-related updates
         private int animTargetProgress;
         private int lastAudibleLevel = 1;
-        private SeekableSliderHapticPlugin mHapticPlugin;
+        private SeekbarHapticPlugin mHapticPlugin;
         private int mProgressHapticsType = PROGRESS_HAPTICS_DISABLED;
 
         void setIcon(int iconRes, Resources.Theme theme) {
@@ -2658,7 +2658,7 @@
                 com.android.systemui.util.time.SystemClock systemClock) {
             if (mHapticPlugin != null) return;
 
-            mHapticPlugin = new SeekableSliderHapticPlugin(
+            mHapticPlugin = new SeekbarHapticPlugin(
                 vibratorHelper,
                 systemClock,
                 sSliderHapticFeedbackConfig);
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/CarrierTextManagerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/CarrierTextManagerTest.java
index 303ae97..11d31c6 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/CarrierTextManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/CarrierTextManagerTest.java
@@ -58,12 +58,15 @@
 import com.android.keyguard.logging.CarrierTextManagerLogger;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
+import com.android.systemui.kosmos.KosmosJavaAdapter;
 import com.android.systemui.log.LogBufferHelperKt;
 import com.android.systemui.res.R;
+import com.android.systemui.statusbar.pipeline.satellite.ui.viewmodel.FakeDeviceBasedSatelliteViewModel;
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository;
 import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel;
 import com.android.systemui.telephony.TelephonyListenerManager;
 import com.android.systemui.util.concurrency.FakeExecutor;
+import com.android.systemui.util.kotlin.JavaAdapter;
 import com.android.systemui.util.time.FakeSystemClock;
 
 import org.junit.Before;
@@ -78,6 +81,8 @@
 import java.util.HashMap;
 import java.util.List;
 
+import kotlinx.coroutines.test.TestScope;
+
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 public class CarrierTextManagerTest extends SysuiTestCase {
@@ -99,6 +104,8 @@
             TEST_CARRIER, TEST_CARRIER, NAME_SOURCE_CARRIER_ID, 0xFFFFFF, "",
             DATA_ROAMING_ENABLE, null, null, null, null, false, null, "");
     private FakeWifiRepository mWifiRepository = new FakeWifiRepository();
+    private final FakeDeviceBasedSatelliteViewModel mSatelliteViewModel =
+            new FakeDeviceBasedSatelliteViewModel();
     @Mock
     private WakefulnessLifecycle mWakefulnessLifecycle;
     @Mock
@@ -124,6 +131,10 @@
             new CarrierTextManagerLogger(
                     LogBufferHelperKt.logcatLogBuffer("CarrierTextManagerLog"));
 
+    private final KosmosJavaAdapter mKosmos = new KosmosJavaAdapter(this);
+    private final TestScope mTestScope = mKosmos.getTestScope();
+    private final JavaAdapter mJavaAdapter = new JavaAdapter(mTestScope.getBackgroundScope());
+
     private Void checkMainThread(InvocationOnMock inv) {
         assertThat(mMainExecutor.isExecuting()).isTrue();
         assertThat(mBgExecutor.isExecuting()).isFalse();
@@ -156,9 +167,18 @@
         when(mTelephonyManager.getActiveModemCount()).thenReturn(3);
 
         mCarrierTextManager = new CarrierTextManager.Builder(
-                mContext, mContext.getResources(), mWifiRepository,
-                mTelephonyManager, mTelephonyListenerManager, mWakefulnessLifecycle, mMainExecutor,
-                mBgExecutor, mKeyguardUpdateMonitor, mLogger)
+                mContext,
+                mContext.getResources(),
+                mWifiRepository,
+                mSatelliteViewModel,
+                mJavaAdapter,
+                mTelephonyManager,
+                mTelephonyListenerManager,
+                mWakefulnessLifecycle,
+                mMainExecutor,
+                mBgExecutor,
+                mKeyguardUpdateMonitor,
+                mLogger)
                 .setShowAirplaneMode(true)
                 .setShowMissingSim(true)
                 .build();
@@ -211,6 +231,8 @@
                 getContextSpyForStickyBroadcast(stickyIntent),
                 mContext.getResources(),
                 mWifiRepository,
+                mSatelliteViewModel,
+                mJavaAdapter,
                 mTelephonyManager,
                 mTelephonyListenerManager,
                 mWakefulnessLifecycle,
@@ -451,6 +473,107 @@
     }
 
     @Test
+    public void carrierText_satelliteTextNull_notUsed() {
+        reset(mCarrierTextCallback);
+        List<SubscriptionInfo> list = new ArrayList<>();
+        list.add(TEST_SUBSCRIPTION);
+        when(mKeyguardUpdateMonitor.getSimState(anyInt())).thenReturn(
+                TelephonyManager.SIM_STATE_READY);
+        when(mKeyguardUpdateMonitor.getFilteredSubscriptionInfo()).thenReturn(list);
+        mKeyguardUpdateMonitor.mServiceStates = new HashMap<>();
+
+        // WHEN the satellite text is null
+        mSatelliteViewModel.getCarrierText().setValue(null);
+        mTestScope.getTestScheduler().runCurrent();
+
+        ArgumentCaptor<CarrierTextManager.CarrierTextCallbackInfo> captor =
+                ArgumentCaptor.forClass(
+                        CarrierTextManager.CarrierTextCallbackInfo.class);
+        FakeExecutor.exhaustExecutors(mMainExecutor, mBgExecutor);
+
+        // THEN the default subscription carrier text is used
+        verify(mCarrierTextCallback).updateCarrierInfo(captor.capture());
+        assertThat(captor.getValue().carrierText).isEqualTo(TEST_CARRIER);
+    }
+
+    @Test
+    public void carrierText_satelliteTextUpdates_autoTriggersCallback() {
+        reset(mCarrierTextCallback);
+        List<SubscriptionInfo> list = new ArrayList<>();
+        list.add(TEST_SUBSCRIPTION);
+        when(mKeyguardUpdateMonitor.getSimState(anyInt())).thenReturn(
+                TelephonyManager.SIM_STATE_READY);
+        when(mKeyguardUpdateMonitor.getFilteredSubscriptionInfo()).thenReturn(list);
+        mKeyguardUpdateMonitor.mServiceStates = new HashMap<>();
+
+        // WHEN the satellite text is set
+        mSatelliteViewModel.getCarrierText().setValue("Test satellite text");
+        mTestScope.getTestScheduler().runCurrent();
+
+        // THEN we should automatically re-trigger #updateCarrierText and get callback info
+        ArgumentCaptor<CarrierTextManager.CarrierTextCallbackInfo> captor =
+                ArgumentCaptor.forClass(
+                        CarrierTextManager.CarrierTextCallbackInfo.class);
+        FakeExecutor.exhaustExecutors(mMainExecutor, mBgExecutor);
+        verify(mCarrierTextCallback).updateCarrierInfo(captor.capture());
+        // AND use the satellite text as the carrier text
+        assertThat(captor.getValue().carrierText).isEqualTo("Test satellite text");
+
+        // WHEN the satellite text is reset to null
+        reset(mCarrierTextCallback);
+        mSatelliteViewModel.getCarrierText().setValue(null);
+        mTestScope.getTestScheduler().runCurrent();
+
+        // THEN we should automatically re-trigger #updateCarrierText and get callback info
+        // that doesn't include the satellite info
+        FakeExecutor.exhaustExecutors(mMainExecutor, mBgExecutor);
+        verify(mCarrierTextCallback).updateCarrierInfo(captor.capture());
+        assertThat(captor.getValue().carrierText).isEqualTo(TEST_CARRIER);
+    }
+
+    @Test
+    public void carrierText_updatedWhileNotListening_getsNewValueWhenListening() {
+        reset(mCarrierTextCallback);
+        List<SubscriptionInfo> list = new ArrayList<>();
+        list.add(TEST_SUBSCRIPTION);
+        when(mKeyguardUpdateMonitor.getSimState(anyInt())).thenReturn(
+                TelephonyManager.SIM_STATE_READY);
+        when(mKeyguardUpdateMonitor.getFilteredSubscriptionInfo()).thenReturn(list);
+        mKeyguardUpdateMonitor.mServiceStates = new HashMap<>();
+
+        mSatelliteViewModel.getCarrierText().setValue("Old satellite text");
+        mTestScope.getTestScheduler().runCurrent();
+
+        ArgumentCaptor<CarrierTextManager.CarrierTextCallbackInfo> captor =
+                ArgumentCaptor.forClass(
+                        CarrierTextManager.CarrierTextCallbackInfo.class);
+        FakeExecutor.exhaustExecutors(mMainExecutor, mBgExecutor);
+        verify(mCarrierTextCallback).updateCarrierInfo(captor.capture());
+        assertThat(captor.getValue().carrierText).isEqualTo("Old satellite text");
+
+        // WHEN we stop listening
+        reset(mCarrierTextCallback);
+        mCarrierTextManager.setListening(null);
+
+        // AND the satellite text updates
+        mSatelliteViewModel.getCarrierText().setValue("New satellite text");
+
+        // THEN we don't get new callback info because we aren't listening
+        verify(mCarrierTextCallback, never()).updateCarrierInfo(any());
+
+        // WHEN we start listening again
+        reset(mCarrierTextCallback);
+        mCarrierTextManager.setListening(mCarrierTextCallback);
+
+        // THEN we should automatically re-trigger #updateCarrierText and get callback info
+        // that includes the new satellite text
+        mTestScope.getTestScheduler().runCurrent();
+        FakeExecutor.exhaustExecutors(mMainExecutor, mBgExecutor);
+        verify(mCarrierTextCallback).updateCarrierInfo(captor.capture());
+        assertThat(captor.getValue().carrierText).isEqualTo("New satellite text");
+    }
+
+    @Test
     public void testCreateInfo_noSubscriptions() {
         reset(mCarrierTextCallback);
         when(mKeyguardUpdateMonitor.getFilteredSubscriptionInfo()).thenReturn(
@@ -471,6 +594,28 @@
     }
 
     @Test
+    public void testCarrierText_oneValidSubscription() {
+        reset(mCarrierTextCallback);
+        List<SubscriptionInfo> list = new ArrayList<>();
+        list.add(TEST_SUBSCRIPTION);
+        when(mKeyguardUpdateMonitor.getSimState(anyInt())).thenReturn(
+                TelephonyManager.SIM_STATE_READY);
+        when(mKeyguardUpdateMonitor.getFilteredSubscriptionInfo()).thenReturn(list);
+
+        mKeyguardUpdateMonitor.mServiceStates = new HashMap<>();
+
+        ArgumentCaptor<CarrierTextManager.CarrierTextCallbackInfo> captor =
+                ArgumentCaptor.forClass(
+                        CarrierTextManager.CarrierTextCallbackInfo.class);
+
+        mCarrierTextManager.updateCarrierText();
+        FakeExecutor.exhaustExecutors(mMainExecutor, mBgExecutor);
+        verify(mCarrierTextCallback).updateCarrierInfo(captor.capture());
+
+        assertThat(captor.getValue().carrierText).isEqualTo(TEST_CARRIER);
+    }
+
+    @Test
     public void testCarrierText_twoValidSubscriptions() {
         reset(mCarrierTextCallback);
         List<SubscriptionInfo> list = new ArrayList<>();
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
index 68b6d9d..d354fa2 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
@@ -2016,6 +2016,16 @@
     }
 
     @Test
+    public void unfoldFromPostureChange_sendActionToFaceAuthInteractor() {
+        // WHEN device posture changes to unfold
+        deviceInPostureStateOpened();
+        mTestableLooper.processAllMessages();
+
+        // THEN request face auth
+        verify(mFaceAuthInteractor).onDeviceUnfolded();
+    }
+
+    @Test
     public void detectFingerprint_onTemporaryLockoutReset_authenticateFingerprint() {
         ArgumentCaptor<FingerprintManager.LockoutResetCallback> fpLockoutResetCallbackCaptor =
                 ArgumentCaptor.forClass(FingerprintManager.LockoutResetCallback.class);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/SystemUIApplicationTest.kt b/packages/SystemUI/tests/src/com/android/systemui/SystemUIApplicationTest.kt
index e157fc5..4f7610a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/SystemUIApplicationTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/SystemUIApplicationTest.kt
@@ -27,7 +27,6 @@
 import com.android.systemui.flags.systemPropertiesHelper
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.process.processWrapper
-import com.android.systemui.startable.Dependencies
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
 import javax.inject.Provider
@@ -56,6 +55,19 @@
     @Mock private lateinit var bootCompleteCache: BootCompleteCacheImpl
     @Mock private lateinit var initController: InitController
 
+    class StartableA : TestableStartable()
+    class StartableB : TestableStartable()
+    class StartableC : TestableStartable()
+    class StartableD : TestableStartable()
+    class StartableE : TestableStartable()
+
+    val dependencyMap: Map<Class<*>, Set<Class<out CoreStartable>>> =
+        mapOf(
+            StartableC::class.java to setOf(StartableA::class.java),
+            StartableD::class.java to setOf(StartableA::class.java, StartableB::class.java),
+            StartableE::class.java to setOf(StartableD::class.java, StartableB::class.java),
+        )
+
     private val startableA = StartableA()
     private val startableB = StartableB()
     private val startableC = StartableC()
@@ -76,6 +88,7 @@
         whenever(sysuiComponent.provideBootCacheImpl()).thenReturn(bootCompleteCache)
         whenever(sysuiComponent.createDumpManager()).thenReturn(kosmos.dumpManager)
         whenever(sysuiComponent.initController).thenReturn(initController)
+        whenever(sysuiComponent.startableDependencies).thenReturn(dependencyMap)
         kosmos.processWrapper.systemUser = true
 
         app.setContextAvailableCallback(contextAvailableCallback)
@@ -168,13 +181,4 @@
             startOrder++
         }
     }
-
-    class StartableA : TestableStartable()
-    class StartableB : TestableStartable()
-
-    @Dependencies(StartableA::class) class StartableC : TestableStartable()
-
-    @Dependencies(StartableA::class, StartableB::class) class StartableD : TestableStartable()
-
-    @Dependencies(StartableD::class, StartableB::class) class StartableE : TestableStartable()
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SeekableSliderEventProducerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SeekableSliderEventProducerTest.kt
deleted file mode 100644
index c22d35c..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SeekableSliderEventProducerTest.kt
+++ /dev/null
@@ -1,147 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.haptics.slider
-
-import android.widget.SeekBar
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.coroutines.collectLastValue
-import junit.framework.Assert.assertEquals
-import kotlinx.coroutines.test.runTest
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-class SeekableSliderEventProducerTest : SysuiTestCase() {
-
-    private val seekBar = SeekBar(mContext)
-    private val eventProducer = SeekableSliderEventProducer()
-    private val eventFlow = eventProducer.produceEvents()
-
-    @Test
-    fun onStartTrackingTouch_noProgress_trackingTouchEventProduced() = runTest {
-        val latest by collectLastValue(eventFlow)
-
-        eventProducer.onStartTrackingTouch(seekBar)
-
-        assertEquals(SliderEvent(SliderEventType.STARTED_TRACKING_TOUCH, 0F), latest)
-    }
-
-    @Test
-    fun onStopTrackingTouch_noProgress_StoppedTrackingTouchEventProduced() = runTest {
-        val latest by collectLastValue(eventFlow)
-
-        eventProducer.onStopTrackingTouch(seekBar)
-
-        assertEquals(SliderEvent(SliderEventType.STOPPED_TRACKING_TOUCH, 0F), latest)
-    }
-
-    @Test
-    fun onProgressChangeByUser_changeByUserEventProduced_withNormalizedProgress() = runTest {
-        val progress = 50
-        val latest by collectLastValue(eventFlow)
-
-        eventProducer.onProgressChanged(seekBar, progress, true)
-
-        assertEquals(SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_USER, 0.5F), latest)
-    }
-
-    @Test
-    fun onProgressChangeByUser_zeroWidthSlider_changeByUserEventProduced_withMaxProgress() =
-        runTest {
-            // No-width slider where the min and max values are the same
-            seekBar.min = 100
-            seekBar.max = 100
-            val progress = 50
-            val latest by collectLastValue(eventFlow)
-
-            eventProducer.onProgressChanged(seekBar, progress, true)
-
-            assertEquals(SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_USER, 1.0F), latest)
-        }
-
-    @Test
-    fun onProgressChangeByProgram_changeByProgramEventProduced_withNormalizedProgress() = runTest {
-        val progress = 50
-        val latest by collectLastValue(eventFlow)
-
-        eventProducer.onProgressChanged(seekBar, progress, false)
-
-        assertEquals(SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_PROGRAM, 0.5F), latest)
-    }
-
-    @Test
-    fun onProgressChangeByProgram_zeroWidthSlider_changeByProgramEventProduced_withMaxProgress() =
-        runTest {
-            // No-width slider where the min and max values are the same
-            seekBar.min = 100
-            seekBar.max = 100
-            val progress = 50
-            val latest by collectLastValue(eventFlow)
-
-            eventProducer.onProgressChanged(seekBar, progress, false)
-
-            assertEquals(SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_PROGRAM, 1.0F), latest)
-        }
-
-    @Test
-    fun onStartTrackingTouch_afterProgress_trackingTouchEventProduced_withNormalizedProgress() =
-        runTest {
-            val progress = 50
-            val latest by collectLastValue(eventFlow)
-
-            eventProducer.onProgressChanged(seekBar, progress, true)
-            eventProducer.onStartTrackingTouch(seekBar)
-
-            assertEquals(SliderEvent(SliderEventType.STARTED_TRACKING_TOUCH, 0.5F), latest)
-        }
-
-    @Test
-    fun onStopTrackingTouch_afterProgress_stopTrackingTouchEventProduced_withNormalizedProgress() =
-        runTest {
-            val progress = 50
-            val latest by collectLastValue(eventFlow)
-
-            eventProducer.onProgressChanged(seekBar, progress, true)
-            eventProducer.onStopTrackingTouch(seekBar)
-
-            assertEquals(SliderEvent(SliderEventType.STOPPED_TRACKING_TOUCH, 0.5F), latest)
-        }
-
-    @Test
-    fun onArrowUp_afterStartTrackingTouch_ArrowUpProduced() = runTest {
-        val latest by collectLastValue(eventFlow)
-
-        eventProducer.onStartTrackingTouch(seekBar)
-        eventProducer.onArrowUp()
-
-        assertEquals(SliderEvent(SliderEventType.ARROW_UP, 0f), latest)
-    }
-
-    @Test
-    fun onArrowUp_afterChangeByProgram_ArrowUpProduced_withProgress() = runTest {
-        val progress = 50
-        val latest by collectLastValue(eventFlow)
-
-        eventProducer.onProgressChanged(seekBar, progress, false)
-        eventProducer.onArrowUp()
-
-        assertEquals(SliderEvent(SliderEventType.ARROW_UP, 0.5f), latest)
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SeekableSliderTrackerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SliderStateTrackerTest.kt
similarity index 79%
rename from packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SeekableSliderTrackerTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SliderStateTrackerTest.kt
index 796d6d9..a09d345 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SeekableSliderTrackerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SliderStateTrackerTest.kt
@@ -38,11 +38,11 @@
 @SmallTest
 @OptIn(ExperimentalCoroutinesApi::class)
 @RunWith(AndroidJUnit4::class)
-class SeekableSliderTrackerTest : SysuiTestCase() {
+class SliderStateTrackerTest : SysuiTestCase() {
 
     @Mock private lateinit var sliderStateListener: SliderStateListener
     private val sliderEventProducer = FakeSliderEventProducer()
-    private lateinit var mSeekableSliderTracker: SeekableSliderTracker
+    private lateinit var mSliderStateTracker: SliderStateTracker
 
     @Before
     fun setup() {
@@ -55,7 +55,7 @@
         initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
 
         // THEN the tracker job is active
-        assertThat(mSeekableSliderTracker.isTracking).isTrue()
+        assertThat(mSliderStateTracker.isTracking).isTrue()
     }
 
     @Test
@@ -65,14 +65,14 @@
             initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
 
             // GIVEN a state in the state machine
-            mSeekableSliderTracker.setState(it)
+            mSliderStateTracker.setState(it)
 
             // WHEN the tracker stops tracking the state and listening to events
-            mSeekableSliderTracker.stopTracking()
+            mSliderStateTracker.stopTracking()
 
             // THEN The state is idle and the tracker is not active
-            assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.IDLE)
-            assertThat(mSeekableSliderTracker.isTracking).isFalse()
+            assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.IDLE)
+            assertThat(mSliderStateTracker.isTracking).isFalse()
         }
     }
 
@@ -83,7 +83,7 @@
         initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
 
         // THEN The state is idle and the listener is not called to play haptics
-        assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.IDLE)
+        assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.IDLE)
         verifyZeroInteractions(sliderStateListener)
     }
 
@@ -96,9 +96,9 @@
         sliderEventProducer.sendEvent(SliderEvent(SliderEventType.STARTED_TRACKING_TOUCH, progress))
 
         // THEN the tracker moves to the wait state and the timer job begins
-        assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.WAIT)
+        assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.WAIT)
         verifyZeroInteractions(sliderStateListener)
-        assertThat(mSeekableSliderTracker.isWaiting).isTrue()
+        assertThat(mSliderStateTracker.isWaiting).isTrue()
     }
 
     // Tests on the WAIT state
@@ -117,9 +117,9 @@
         advanceTimeBy(config.waitTimeMillis + 10L)
 
         // THEN the tracker moves to the DRAG_HANDLE_ACQUIRED_BY_TOUCH state
-        assertThat(mSeekableSliderTracker.currentState)
+        assertThat(mSliderStateTracker.currentState)
             .isEqualTo(SliderState.DRAG_HANDLE_ACQUIRED_BY_TOUCH)
-        assertThat(mSeekableSliderTracker.isWaiting).isFalse()
+        assertThat(mSliderStateTracker.isWaiting).isFalse()
         verify(sliderStateListener).onHandleAcquiredByTouch()
         verifyNoMoreInteractions(sliderStateListener)
     }
@@ -142,9 +142,9 @@
 
         // THEN the tracker moves to the DRAG_HANDLE_ACQUIRED_BY_TOUCH state without the timer job
         // being complete
-        assertThat(mSeekableSliderTracker.currentState)
+        assertThat(mSliderStateTracker.currentState)
             .isEqualTo(SliderState.DRAG_HANDLE_ACQUIRED_BY_TOUCH)
-        assertThat(mSeekableSliderTracker.isWaiting).isFalse()
+        assertThat(mSliderStateTracker.isWaiting).isFalse()
         verify(sliderStateListener).onHandleAcquiredByTouch()
         verifyNoMoreInteractions(sliderStateListener)
     }
@@ -166,9 +166,9 @@
         )
 
         // THEN the tracker moves to the jump-track location selected state
-        assertThat(mSeekableSliderTracker.currentState)
+        assertThat(mSliderStateTracker.currentState)
             .isEqualTo(SliderState.JUMP_TRACK_LOCATION_SELECTED)
-        assertThat(mSeekableSliderTracker.isWaiting).isFalse()
+        assertThat(mSliderStateTracker.isWaiting).isFalse()
         verify(sliderStateListener).onProgressJump(anyFloat())
         verifyNoMoreInteractions(sliderStateListener)
     }
@@ -190,8 +190,8 @@
         )
 
         // THEN the tracker moves to the jump-track location selected state
-        assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.JUMP_BOOKEND_SELECTED)
-        assertThat(mSeekableSliderTracker.isWaiting).isFalse()
+        assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.JUMP_BOOKEND_SELECTED)
+        assertThat(mSliderStateTracker.isWaiting).isFalse()
         verifyNoMoreInteractions(sliderStateListener)
     }
 
@@ -212,8 +212,8 @@
         )
 
         // THEN the tracker moves to the JUMP_TRACK_LOCATION_SELECTED state
-        assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.JUMP_BOOKEND_SELECTED)
-        assertThat(mSeekableSliderTracker.isWaiting).isFalse()
+        assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.JUMP_BOOKEND_SELECTED)
+        assertThat(mSliderStateTracker.isWaiting).isFalse()
         verifyNoMoreInteractions(sliderStateListener)
     }
 
@@ -225,15 +225,15 @@
         // GIVEN a start of tracking touch event that moves the tracker to WAIT at the middle of the
         // slider
         sliderEventProducer.sendEvent(SliderEvent(SliderEventType.STARTED_TRACKING_TOUCH, 0.5f))
-        assertThat(mSeekableSliderTracker.isWaiting).isTrue()
+        assertThat(mSliderStateTracker.isWaiting).isTrue()
 
         // GIVEN that the tracker stops tracking the state and listening to events
-        mSeekableSliderTracker.stopTracking()
+        mSliderStateTracker.stopTracking()
 
         // THEN the tracker moves to the IDLE state without the timer job being complete
-        assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.IDLE)
-        assertThat(mSeekableSliderTracker.isWaiting).isFalse()
-        assertThat(mSeekableSliderTracker.isTracking).isFalse()
+        assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.IDLE)
+        assertThat(mSliderStateTracker.isWaiting).isFalse()
+        assertThat(mSliderStateTracker.isTracking).isFalse()
         verifyNoMoreInteractions(sliderStateListener)
     }
 
@@ -244,13 +244,13 @@
         initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
 
         // GIVEN a JUMP_TRACK_LOCATION_SELECTED state
-        mSeekableSliderTracker.setState(SliderState.JUMP_TRACK_LOCATION_SELECTED)
+        mSliderStateTracker.setState(SliderState.JUMP_TRACK_LOCATION_SELECTED)
 
         // GIVEN a progress event due to dragging the handle
         sliderEventProducer.sendEvent(SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_USER, 0.5f))
 
         // THEN the tracker moves to the DRAG_HANDLE_DRAGGING state
-        assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.DRAG_HANDLE_DRAGGING)
+        assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.DRAG_HANDLE_DRAGGING)
         verify(sliderStateListener).onProgress(anyFloat())
         verifyNoMoreInteractions(sliderStateListener)
     }
@@ -260,14 +260,14 @@
         initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
 
         // GIVEN a JUMP_TRACK_LOCATION_SELECTED state
-        mSeekableSliderTracker.setState(SliderState.JUMP_TRACK_LOCATION_SELECTED)
+        mSliderStateTracker.setState(SliderState.JUMP_TRACK_LOCATION_SELECTED)
 
         // GIVEN that the slider stopped tracking touch
         sliderEventProducer.sendEvent(SliderEvent(SliderEventType.STOPPED_TRACKING_TOUCH, 0.5f))
 
         // THEN the tracker executes on onHandleReleasedFromTouch before moving to the IDLE state
         verify(sliderStateListener).onHandleReleasedFromTouch()
-        assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.IDLE)
+        assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.IDLE)
         verifyNoMoreInteractions(sliderStateListener)
     }
 
@@ -276,13 +276,13 @@
         initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
 
         // GIVEN a JUMP_BOOKEND_SELECTED state
-        mSeekableSliderTracker.setState(SliderState.JUMP_BOOKEND_SELECTED)
+        mSliderStateTracker.setState(SliderState.JUMP_BOOKEND_SELECTED)
 
         // GIVEN that the slider stopped tracking touch
         sliderEventProducer.sendEvent(SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_USER, 0.5f))
 
         // THEN the tracker moves to the DRAG_HANDLE_DRAGGING state
-        assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.DRAG_HANDLE_DRAGGING)
+        assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.DRAG_HANDLE_DRAGGING)
         verify(sliderStateListener).onProgress(anyFloat())
         verifyNoMoreInteractions(sliderStateListener)
     }
@@ -292,14 +292,14 @@
         initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
 
         // GIVEN a JUMP_BOOKEND_SELECTED state
-        mSeekableSliderTracker.setState(SliderState.JUMP_BOOKEND_SELECTED)
+        mSliderStateTracker.setState(SliderState.JUMP_BOOKEND_SELECTED)
 
         // GIVEN that the slider stopped tracking touch
         sliderEventProducer.sendEvent(SliderEvent(SliderEventType.STOPPED_TRACKING_TOUCH, 0.5f))
 
         // THEN the tracker executes on onHandleReleasedFromTouch before moving to the IDLE state
         verify(sliderStateListener).onHandleReleasedFromTouch()
-        assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.IDLE)
+        assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.IDLE)
         verifyNoMoreInteractions(sliderStateListener)
     }
 
@@ -310,7 +310,7 @@
         initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
 
         // GIVEN a DRAG_HANDLE_ACQUIRED_BY_TOUCH state
-        mSeekableSliderTracker.setState(SliderState.DRAG_HANDLE_ACQUIRED_BY_TOUCH)
+        mSliderStateTracker.setState(SliderState.DRAG_HANDLE_ACQUIRED_BY_TOUCH)
 
         // GIVEN a progress change by the user
         val progress = 0.5f
@@ -320,7 +320,7 @@
 
         // THEN the tracker moves to the DRAG_HANDLE_DRAGGING state
         verify(sliderStateListener).onProgress(progress)
-        assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.DRAG_HANDLE_DRAGGING)
+        assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.DRAG_HANDLE_DRAGGING)
         verifyNoMoreInteractions(sliderStateListener)
     }
 
@@ -329,14 +329,14 @@
         initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
 
         // GIVEN a DRAG_HANDLE_ACQUIRED_BY_TOUCH state
-        mSeekableSliderTracker.setState(SliderState.DRAG_HANDLE_ACQUIRED_BY_TOUCH)
+        mSliderStateTracker.setState(SliderState.DRAG_HANDLE_ACQUIRED_BY_TOUCH)
 
         // GIVEN that the handle stops tracking touch
         sliderEventProducer.sendEvent(SliderEvent(SliderEventType.STOPPED_TRACKING_TOUCH, 0.5f))
 
         // THEN the tracker executes on onHandleReleasedFromTouch before moving to the IDLE state
         verify(sliderStateListener).onHandleReleasedFromTouch()
-        assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.IDLE)
+        assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.IDLE)
         verifyNoMoreInteractions(sliderStateListener)
     }
 
@@ -348,7 +348,7 @@
             initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
 
             // GIVEN a DRAG_HANDLE_DRAGGING state
-            mSeekableSliderTracker.setState(SliderState.DRAG_HANDLE_DRAGGING)
+            mSliderStateTracker.setState(SliderState.DRAG_HANDLE_DRAGGING)
 
             // GIVEN a progress change by the user outside of bookend bounds
             val progress = 0.5f
@@ -357,8 +357,7 @@
             )
 
             // THEN the tracker does not change state and executes the onProgress call
-            assertThat(mSeekableSliderTracker.currentState)
-                .isEqualTo(SliderState.DRAG_HANDLE_DRAGGING)
+            assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.DRAG_HANDLE_DRAGGING)
             verify(sliderStateListener).onProgress(progress)
             verifyNoMoreInteractions(sliderStateListener)
         }
@@ -370,7 +369,7 @@
             initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config)
 
             // GIVEN a DRAG_HANDLE_DRAGGING state
-            mSeekableSliderTracker.setState(SliderState.DRAG_HANDLE_DRAGGING)
+            mSliderStateTracker.setState(SliderState.DRAG_HANDLE_DRAGGING)
 
             // GIVEN a progress change by the user reaching the lower bookend
             val progress = config.lowerBookendThreshold - 0.01f
@@ -380,7 +379,7 @@
 
             // THEN the tracker moves to the DRAG_HANDLE_REACHED_BOOKEND state and executes the
             // corresponding callback
-            assertThat(mSeekableSliderTracker.currentState)
+            assertThat(mSliderStateTracker.currentState)
                 .isEqualTo(SliderState.DRAG_HANDLE_REACHED_BOOKEND)
             verify(sliderStateListener).onLowerBookend()
             verifyNoMoreInteractions(sliderStateListener)
@@ -393,7 +392,7 @@
             initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config)
 
             // GIVEN a DRAG_HANDLE_DRAGGING state
-            mSeekableSliderTracker.setState(SliderState.DRAG_HANDLE_DRAGGING)
+            mSliderStateTracker.setState(SliderState.DRAG_HANDLE_DRAGGING)
 
             // GIVEN a progress change by the user reaching the upper bookend
             val progress = config.upperBookendThreshold + 0.01f
@@ -403,7 +402,7 @@
 
             // THEN the tracker moves to the DRAG_HANDLE_REACHED_BOOKEND state and executes the
             // corresponding callback
-            assertThat(mSeekableSliderTracker.currentState)
+            assertThat(mSliderStateTracker.currentState)
                 .isEqualTo(SliderState.DRAG_HANDLE_REACHED_BOOKEND)
             verify(sliderStateListener).onUpperBookend()
             verifyNoMoreInteractions(sliderStateListener)
@@ -414,14 +413,14 @@
         initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
 
         // GIVEN a DRAG_HANDLE_DRAGGING state
-        mSeekableSliderTracker.setState(SliderState.DRAG_HANDLE_DRAGGING)
+        mSliderStateTracker.setState(SliderState.DRAG_HANDLE_DRAGGING)
 
         // GIVEN that the slider stops tracking touch
         sliderEventProducer.sendEvent(SliderEvent(SliderEventType.STOPPED_TRACKING_TOUCH, 0.5f))
 
         // THEN the tracker executes on onHandleReleasedFromTouch before moving to the IDLE state
         verify(sliderStateListener).onHandleReleasedFromTouch()
-        assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.IDLE)
+        assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.IDLE)
         verifyNoMoreInteractions(sliderStateListener)
     }
 
@@ -434,7 +433,7 @@
             initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config)
 
             // GIVEN a DRAG_HANDLE_REACHED_BOOKEND state
-            mSeekableSliderTracker.setState(SliderState.DRAG_HANDLE_REACHED_BOOKEND)
+            mSliderStateTracker.setState(SliderState.DRAG_HANDLE_REACHED_BOOKEND)
 
             // GIVEN a progress event that falls outside of the lower bookend range
             val progress = config.lowerBookendThreshold + 0.01f
@@ -444,8 +443,7 @@
 
             // THEN the tracker moves to the DRAG_HANDLE_DRAGGING state and executes accordingly
             verify(sliderStateListener).onProgress(progress)
-            assertThat(mSeekableSliderTracker.currentState)
-                .isEqualTo(SliderState.DRAG_HANDLE_DRAGGING)
+            assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.DRAG_HANDLE_DRAGGING)
             verifyNoMoreInteractions(sliderStateListener)
         }
 
@@ -455,7 +453,7 @@
         initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config)
 
         // GIVEN a DRAG_HANDLE_REACHED_BOOKEND state
-        mSeekableSliderTracker.setState(SliderState.DRAG_HANDLE_REACHED_BOOKEND)
+        mSliderStateTracker.setState(SliderState.DRAG_HANDLE_REACHED_BOOKEND)
 
         // GIVEN a progress event that falls inside of the lower bookend range
         val progress = config.lowerBookendThreshold - 0.01f
@@ -465,7 +463,7 @@
 
         // THEN the tracker stays in the current state and executes accordingly
         verify(sliderStateListener).onLowerBookend()
-        assertThat(mSeekableSliderTracker.currentState)
+        assertThat(mSliderStateTracker.currentState)
             .isEqualTo(SliderState.DRAG_HANDLE_REACHED_BOOKEND)
         verifyNoMoreInteractions(sliderStateListener)
     }
@@ -477,7 +475,7 @@
             initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config)
 
             // GIVEN a DRAG_HANDLE_REACHED_BOOKEND state
-            mSeekableSliderTracker.setState(SliderState.DRAG_HANDLE_REACHED_BOOKEND)
+            mSliderStateTracker.setState(SliderState.DRAG_HANDLE_REACHED_BOOKEND)
 
             // GIVEN a progress event that falls outside of the upper bookend range
             val progress = config.upperBookendThreshold - 0.01f
@@ -487,8 +485,7 @@
 
             // THEN the tracker moves to the DRAG_HANDLE_DRAGGING state and executes accordingly
             verify(sliderStateListener).onProgress(progress)
-            assertThat(mSeekableSliderTracker.currentState)
-                .isEqualTo(SliderState.DRAG_HANDLE_DRAGGING)
+            assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.DRAG_HANDLE_DRAGGING)
             verifyNoMoreInteractions(sliderStateListener)
         }
 
@@ -498,7 +495,7 @@
         initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config)
 
         // GIVEN a DRAG_HANDLE_REACHED_BOOKEND state
-        mSeekableSliderTracker.setState(SliderState.DRAG_HANDLE_REACHED_BOOKEND)
+        mSliderStateTracker.setState(SliderState.DRAG_HANDLE_REACHED_BOOKEND)
 
         // GIVEN a progress event that falls inside of the upper bookend range
         val progress = config.upperBookendThreshold + 0.01f
@@ -508,7 +505,7 @@
 
         // THEN the tracker stays in the current state and executes accordingly
         verify(sliderStateListener).onUpperBookend()
-        assertThat(mSeekableSliderTracker.currentState)
+        assertThat(mSliderStateTracker.currentState)
             .isEqualTo(SliderState.DRAG_HANDLE_REACHED_BOOKEND)
         verifyNoMoreInteractions(sliderStateListener)
     }
@@ -518,37 +515,36 @@
         initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
 
         // GIVEN a DRAG_HANDLE_REACHED_BOOKEND state
-        mSeekableSliderTracker.setState(SliderState.DRAG_HANDLE_REACHED_BOOKEND)
+        mSliderStateTracker.setState(SliderState.DRAG_HANDLE_REACHED_BOOKEND)
 
         // GIVEN that the handle stops tracking touch
         sliderEventProducer.sendEvent(SliderEvent(SliderEventType.STOPPED_TRACKING_TOUCH, 0.5f))
 
         // THEN the tracker executes on onHandleReleasedFromTouch before moving to the IDLE state
         verify(sliderStateListener).onHandleReleasedFromTouch()
-        assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.IDLE)
+        assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.IDLE)
         verifyNoMoreInteractions(sliderStateListener)
     }
 
     @Test
-    fun onProgressChangeByProgram_atTheMiddle_onIdle_movesToArrowHandleMovedOnce() = runTest {
+    fun onStartedTrackingProgram_atTheMiddle_onIdle_movesToArrowHandleMovedOnce() = runTest {
         // GIVEN an initialized tracker in the IDLE state
         initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
 
         // GIVEN a progress due to an external source that lands at the middle of the slider
         val progress = 0.5f
         sliderEventProducer.sendEvent(
-            SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_PROGRAM, progress)
+            SliderEvent(SliderEventType.STARTED_TRACKING_PROGRAM, progress)
         )
 
         // THEN the state moves to ARROW_HANDLE_MOVED_ONCE and the listener is called to play
         // haptics
-        assertThat(mSeekableSliderTracker.currentState)
-            .isEqualTo(SliderState.ARROW_HANDLE_MOVED_ONCE)
+        assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.ARROW_HANDLE_MOVED_ONCE)
         verify(sliderStateListener).onSelectAndArrow(progress)
     }
 
     @Test
-    fun onProgressChangeByProgram_atUpperBookend_onIdle_movesToIdle() = runTest {
+    fun onStartedTrackingProgram_atUpperBookend_onIdle_movesToIdle() = runTest {
         // GIVEN an initialized tracker in the IDLE state
         val config = SeekableSliderTrackerConfig()
         initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config)
@@ -556,16 +552,16 @@
         // GIVEN a progress due to an external source that lands at the upper bookend
         val progress = config.upperBookendThreshold + 0.01f
         sliderEventProducer.sendEvent(
-            SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_PROGRAM, progress)
+            SliderEvent(SliderEventType.STARTED_TRACKING_PROGRAM, progress)
         )
 
         // THEN the tracker executes upper bookend haptics before moving back to IDLE
         verify(sliderStateListener).onUpperBookend()
-        assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.IDLE)
+        assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.IDLE)
     }
 
     @Test
-    fun onProgressChangeByProgram_atLowerBookend_onIdle_movesToIdle() = runTest {
+    fun onStartedTrackingProgram_atLowerBookend_onIdle_movesToIdle() = runTest {
         // GIVEN an initialized tracker in the IDLE state
         val config = SeekableSliderTrackerConfig()
         initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config)
@@ -573,26 +569,28 @@
         // WHEN a progress is recorded due to an external source that lands at the lower bookend
         val progress = config.lowerBookendThreshold - 0.01f
         sliderEventProducer.sendEvent(
-            SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_PROGRAM, progress)
+            SliderEvent(SliderEventType.STARTED_TRACKING_PROGRAM, progress)
         )
 
         // THEN the tracker executes lower bookend haptics before moving to IDLE
         verify(sliderStateListener).onLowerBookend()
-        assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.IDLE)
+        assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.IDLE)
     }
 
     @Test
     fun onArrowUp_onArrowMovedOnce_movesToIdle() = runTest {
         // GIVEN an initialized tracker in the ARROW_HANDLE_MOVED_ONCE state
         initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
-        mSeekableSliderTracker.setState(SliderState.ARROW_HANDLE_MOVED_ONCE)
+        mSliderStateTracker.setState(SliderState.ARROW_HANDLE_MOVED_ONCE)
 
         // WHEN the external stimulus is released
         val progress = 0.5f
-        sliderEventProducer.sendEvent(SliderEvent(SliderEventType.ARROW_UP, progress))
+        sliderEventProducer.sendEvent(
+            SliderEvent(SliderEventType.STOPPED_TRACKING_PROGRAM, progress)
+        )
 
         // THEN the tracker moves back to IDLE and there are no haptics
-        assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.IDLE)
+        assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.IDLE)
         verifyZeroInteractions(sliderStateListener)
     }
 
@@ -600,7 +598,7 @@
     fun onStartTrackingTouch_onArrowMovedOnce_movesToWait() = runTest {
         // GIVEN an initialized tracker in the ARROW_HANDLE_MOVED_ONCE state
         initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
-        mSeekableSliderTracker.setState(SliderState.ARROW_HANDLE_MOVED_ONCE)
+        mSliderStateTracker.setState(SliderState.ARROW_HANDLE_MOVED_ONCE)
 
         // WHEN the slider starts tracking touch
         val progress = 0.5f
@@ -608,8 +606,8 @@
 
         // THEN the tracker moves back to WAIT and starts the waiting job. Also, there are no
         // haptics
-        assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.WAIT)
-        assertThat(mSeekableSliderTracker.isWaiting).isTrue()
+        assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.WAIT)
+        assertThat(mSliderStateTracker.isWaiting).isTrue()
         verifyZeroInteractions(sliderStateListener)
     }
 
@@ -617,7 +615,7 @@
     fun onProgressChangeByProgram_onArrowMovedOnce_movesToArrowMovesContinuously() = runTest {
         // GIVEN an initialized tracker in the ARROW_HANDLE_MOVED_ONCE state
         initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
-        mSeekableSliderTracker.setState(SliderState.ARROW_HANDLE_MOVED_ONCE)
+        mSliderStateTracker.setState(SliderState.ARROW_HANDLE_MOVED_ONCE)
 
         // WHEN the slider gets an external progress change
         val progress = 0.5f
@@ -627,7 +625,7 @@
 
         // THEN the tracker moves to ARROW_HANDLE_MOVES_CONTINUOUSLY and calls the appropriate
         // haptics
-        assertThat(mSeekableSliderTracker.currentState)
+        assertThat(mSliderStateTracker.currentState)
             .isEqualTo(SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY)
         verify(sliderStateListener).onProgress(progress)
     }
@@ -636,14 +634,16 @@
     fun onArrowUp_onArrowMovesContinuously_movesToIdle() = runTest {
         // GIVEN an initialized tracker in the ARROW_HANDLE_MOVES_CONTINUOUSLY state
         initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
-        mSeekableSliderTracker.setState(SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY)
+        mSliderStateTracker.setState(SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY)
 
         // WHEN the external stimulus is released
         val progress = 0.5f
-        sliderEventProducer.sendEvent(SliderEvent(SliderEventType.ARROW_UP, progress))
+        sliderEventProducer.sendEvent(
+            SliderEvent(SliderEventType.STOPPED_TRACKING_PROGRAM, progress)
+        )
 
         // THEN the tracker moves to IDLE and no haptics are played
-        assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.IDLE)
+        assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.IDLE)
         verifyZeroInteractions(sliderStateListener)
     }
 
@@ -651,15 +651,15 @@
     fun onStartTrackingTouch_onArrowMovesContinuously_movesToWait() = runTest {
         // GIVEN an initialized tracker in the ARROW_HANDLE_MOVES_CONTINUOUSLY state
         initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
-        mSeekableSliderTracker.setState(SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY)
+        mSliderStateTracker.setState(SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY)
 
         // WHEN the slider starts tracking touch
         val progress = 0.5f
         sliderEventProducer.sendEvent(SliderEvent(SliderEventType.STARTED_TRACKING_TOUCH, progress))
 
         // THEN the tracker moves to WAIT and the wait job starts.
-        assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.WAIT)
-        assertThat(mSeekableSliderTracker.isWaiting).isTrue()
+        assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.WAIT)
+        assertThat(mSliderStateTracker.isWaiting).isTrue()
         verifyZeroInteractions(sliderStateListener)
     }
 
@@ -667,7 +667,7 @@
     fun onProgressChangeByProgram_onArrowMovesContinuously_preservesState() = runTest {
         // GIVEN an initialized tracker in the ARROW_HANDLE_MOVES_CONTINUOUSLY state
         initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
-        mSeekableSliderTracker.setState(SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY)
+        mSliderStateTracker.setState(SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY)
 
         // WHEN the slider changes progress programmatically at the middle
         val progress = 0.5f
@@ -676,7 +676,7 @@
         )
 
         // THEN the tracker stays in the same state and haptics are delivered appropriately
-        assertThat(mSeekableSliderTracker.currentState)
+        assertThat(mSliderStateTracker.currentState)
             .isEqualTo(SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY)
         verify(sliderStateListener).onProgress(progress)
     }
@@ -686,7 +686,7 @@
         // GIVEN an initialized tracker in the ARROW_HANDLE_MOVES_CONTINUOUSLY state
         val config = SeekableSliderTrackerConfig()
         initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config)
-        mSeekableSliderTracker.setState(SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY)
+        mSliderStateTracker.setState(SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY)
 
         // WHEN the slider reaches the lower bookend programmatically
         val progress = config.lowerBookendThreshold - 0.01f
@@ -696,7 +696,7 @@
 
         // THEN the tracker executes lower bookend haptics before moving to IDLE
         verify(sliderStateListener).onLowerBookend()
-        assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.IDLE)
+        assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.IDLE)
     }
 
     @Test
@@ -704,7 +704,7 @@
         // GIVEN an initialized tracker in the ARROW_HANDLE_MOVES_CONTINUOUSLY state
         val config = SeekableSliderTrackerConfig()
         initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config)
-        mSeekableSliderTracker.setState(SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY)
+        mSliderStateTracker.setState(SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY)
 
         // WHEN the slider reaches the lower bookend programmatically
         val progress = config.upperBookendThreshold + 0.01f
@@ -714,7 +714,7 @@
 
         // THEN the tracker executes upper bookend haptics before moving to IDLE
         verify(sliderStateListener).onUpperBookend()
-        assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.IDLE)
+        assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.IDLE)
     }
 
     @OptIn(ExperimentalCoroutinesApi::class)
@@ -722,8 +722,8 @@
         scope: CoroutineScope,
         config: SeekableSliderTrackerConfig = SeekableSliderTrackerConfig(),
     ) {
-        mSeekableSliderTracker =
-            SeekableSliderTracker(sliderStateListener, sliderEventProducer, scope, config)
-        mSeekableSliderTracker.startTracking()
+        mSliderStateTracker =
+            SliderStateTracker(sliderStateListener, sliderEventProducer, scope, config)
+        mSliderStateTracker.startTracking()
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessSliderControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessSliderControllerTest.kt
index 6a22d86..fb91c78 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessSliderControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessSliderControllerTest.kt
@@ -24,7 +24,7 @@
 import com.android.settingslib.RestrictedLockUtils
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.classifier.FalsingManagerFake
-import com.android.systemui.haptics.slider.SeekableSliderHapticPlugin
+import com.android.systemui.haptics.slider.SeekbarHapticPlugin
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.statusbar.VibratorHelper
 import com.android.systemui.statusbar.policy.BrightnessMirrorController
@@ -93,7 +93,7 @@
                 brightnessSliderView,
                 mFalsingManager,
                 uiEventLogger,
-                SeekableSliderHapticPlugin(vibratorHelper, systemClock),
+                SeekbarHapticPlugin(vibratorHelper, systemClock),
                 activityStarter,
             )
         mController.init()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt
index fd9daf8..03f5ecf 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt
@@ -25,23 +25,24 @@
 import android.view.View
 import android.view.WindowManager
 import android.widget.FrameLayout
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
 import androidx.test.filters.SmallTest
 import com.android.compose.animation.scene.SceneKey
 import com.android.systemui.Flags
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.ambient.touch.TouchHandler
+import com.android.systemui.ambient.touch.TouchMonitor
+import com.android.systemui.ambient.touch.dagger.AmbientTouchComponent
 import com.android.systemui.communal.data.repository.FakeCommunalRepository
 import com.android.systemui.communal.data.repository.fakeCommunalRepository
-import com.android.systemui.communal.domain.interactor.CommunalInteractor
 import com.android.systemui.communal.domain.interactor.communalInteractor
 import com.android.systemui.communal.domain.interactor.setCommunalAvailable
 import com.android.systemui.communal.shared.model.CommunalScenes
 import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
 import com.android.systemui.communal.util.CommunalColors
 import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
 import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
-import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
 import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.model.KeyguardState
@@ -51,7 +52,6 @@
 import com.android.systemui.res.R
 import com.android.systemui.scene.shared.model.sceneDataSourceDelegator
 import com.android.systemui.shade.data.repository.fakeShadeRepository
-import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.shade.domain.interactor.shadeInteractor
 import com.android.systemui.statusbar.phone.SystemUIDialogFactory
 import com.android.systemui.testKosmos
@@ -60,7 +60,6 @@
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.test.UnconfinedTestDispatcher
-import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.After
 import org.junit.Assert.assertThrows
@@ -87,16 +86,14 @@
     @Mock private lateinit var communalViewModel: CommunalViewModel
     @Mock private lateinit var powerManager: PowerManager
     @Mock private lateinit var dialogFactory: SystemUIDialogFactory
+    @Mock private lateinit var touchMonitor: TouchMonitor
     @Mock private lateinit var communalColors: CommunalColors
-    private lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor
-    private lateinit var shadeInteractor: ShadeInteractor
-    private lateinit var keyguardInteractor: KeyguardInteractor
+    private lateinit var ambientTouchComponentFactory: AmbientTouchComponent.Factory
 
     private lateinit var parentView: FrameLayout
     private lateinit var containerView: View
     private lateinit var testableLooper: TestableLooper
 
-    private lateinit var communalInteractor: CommunalInteractor
     private lateinit var communalRepository: FakeCommunalRepository
     private lateinit var underTest: GlanceableHubContainerController
 
@@ -104,32 +101,37 @@
     fun setUp() {
         MockitoAnnotations.initMocks(this)
 
-        communalInteractor = kosmos.communalInteractor
         communalRepository = kosmos.fakeCommunalRepository
-        keyguardTransitionInteractor = kosmos.keyguardTransitionInteractor
-        keyguardInteractor = kosmos.keyguardInteractor
-        shadeInteractor = kosmos.shadeInteractor
 
-        underTest =
-            GlanceableHubContainerController(
-                communalInteractor,
-                communalViewModel,
-                dialogFactory,
-                keyguardTransitionInteractor,
-                keyguardInteractor,
-                shadeInteractor,
-                powerManager,
-                communalColors,
-                kosmos.sceneDataSourceDelegator,
-            )
+        ambientTouchComponentFactory =
+            object : AmbientTouchComponent.Factory {
+                override fun create(
+                    lifecycleOwner: LifecycleOwner,
+                    touchHandlers: Set<TouchHandler>
+                ): AmbientTouchComponent =
+                    object : AmbientTouchComponent {
+                        override fun getTouchMonitor(): TouchMonitor = touchMonitor
+                    }
+            }
+
+        with(kosmos) {
+            underTest =
+                GlanceableHubContainerController(
+                    communalInteractor,
+                    communalViewModel,
+                    dialogFactory,
+                    keyguardTransitionInteractor,
+                    keyguardInteractor,
+                    shadeInteractor,
+                    powerManager,
+                    communalColors,
+                    ambientTouchComponentFactory,
+                    kosmos.sceneDataSourceDelegator,
+                )
+        }
         testableLooper = TestableLooper.get(this)
 
         overrideResource(R.dimen.communal_right_edge_swipe_region_width, RIGHT_SWIPE_REGION_WIDTH)
-        overrideResource(R.dimen.communal_top_edge_swipe_region_height, TOP_SWIPE_REGION_WIDTH)
-        overrideResource(
-            R.dimen.communal_bottom_edge_swipe_region_height,
-            BOTTOM_SWIPE_REGION_WIDTH
-        )
 
         // Make communal available so that communalInteractor.desiredScene accurately reflects
         // scene changes instead of just returning Blank.
@@ -161,6 +163,7 @@
                         shadeInteractor,
                         powerManager,
                         communalColors,
+                        ambientTouchComponentFactory,
                         kosmos.sceneDataSourceDelegator,
                     )
 
@@ -215,62 +218,6 @@
         }
 
     @Test
-    fun onTouchEvent_topSwipeWhenCommunalOpen_doesNotIntercept() =
-        with(kosmos) {
-            testScope.runTest {
-                // Communal is open.
-                goToScene(CommunalScenes.Communal)
-
-                // Touch event in the top swipe region is not intercepted.
-                assertThat(underTest.onTouchEvent(DOWN_IN_TOP_SWIPE_REGION_EVENT)).isFalse()
-            }
-        }
-
-    @Test
-    fun onTouchEvent_bottomSwipeWhenCommunalOpen_doesNotIntercept() =
-        with(kosmos) {
-            testScope.runTest {
-                // Communal is open.
-                goToScene(CommunalScenes.Communal)
-
-                // Touch event in the bottom swipe region is not intercepted.
-                assertThat(underTest.onTouchEvent(DOWN_IN_BOTTOM_SWIPE_REGION_EVENT)).isFalse()
-            }
-        }
-
-    @Test
-    fun onTouchEvent_topSwipeWhenDreaming_doesNotIntercept() =
-        with(kosmos) {
-            testScope.runTest {
-                // Communal is open.
-                goToScene(CommunalScenes.Communal)
-
-                // Device is dreaming.
-                fakeKeyguardRepository.setDreaming(true)
-                runCurrent()
-
-                // Touch event in the top swipe region is not intercepted.
-                assertThat(underTest.onTouchEvent(DOWN_IN_TOP_SWIPE_REGION_EVENT)).isFalse()
-            }
-        }
-
-    @Test
-    fun onTouchEvent_bottomSwipeWhenDreaming_doesNotIntercept() =
-        with(kosmos) {
-            testScope.runTest {
-                // Communal is open.
-                goToScene(CommunalScenes.Communal)
-
-                // Device is dreaming.
-                fakeKeyguardRepository.setDreaming(true)
-                runCurrent()
-
-                // Touch event in the bottom swipe region is not intercepted.
-                assertThat(underTest.onTouchEvent(DOWN_IN_BOTTOM_SWIPE_REGION_EVENT)).isFalse()
-            }
-        }
-
-    @Test
     fun onTouchEvent_communalAndBouncerShowing_doesNotIntercept() =
         with(kosmos) {
             testScope.runTest {
@@ -327,6 +274,141 @@
         }
 
     @Test
+    fun lifecycle_initializedAfterConstruction() =
+        with(kosmos) {
+            val underTest =
+                GlanceableHubContainerController(
+                    communalInteractor,
+                    communalViewModel,
+                    dialogFactory,
+                    keyguardTransitionInteractor,
+                    keyguardInteractor,
+                    shadeInteractor,
+                    powerManager,
+                    communalColors,
+                    ambientTouchComponentFactory,
+                    kosmos.sceneDataSourceDelegator,
+                )
+
+            assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.INITIALIZED)
+        }
+
+    @Test
+    fun lifecycle_createdAfterViewCreated() =
+        with(kosmos) {
+            val underTest =
+                GlanceableHubContainerController(
+                    communalInteractor,
+                    communalViewModel,
+                    dialogFactory,
+                    keyguardTransitionInteractor,
+                    keyguardInteractor,
+                    shadeInteractor,
+                    powerManager,
+                    communalColors,
+                    ambientTouchComponentFactory,
+                    kosmos.sceneDataSourceDelegator,
+                )
+
+            // Only initView without attaching a view as we don't want the flows to start collecting
+            // yet.
+            underTest.initView(View(context))
+
+            assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.CREATED)
+        }
+
+    @Test
+    fun lifecycle_startedAfterFlowsUpdate() {
+        // Flows start collecting due to test setup, causing the state to advance to STARTED.
+        assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+    }
+
+    @Test
+    fun lifecycle_resumedAfterCommunalShows() {
+        // Communal is open.
+        goToScene(CommunalScenes.Communal)
+
+        assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+    }
+
+    @Test
+    fun lifecycle_startedAfterCommunalCloses() =
+        with(kosmos) {
+            testScope.runTest {
+                // Communal is open.
+                goToScene(CommunalScenes.Communal)
+
+                assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+
+                // Communal closes.
+                goToScene(CommunalScenes.Blank)
+
+                assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+            }
+        }
+
+    @Test
+    fun lifecycle_startedAfterPrimaryBouncerShows() =
+        with(kosmos) {
+            testScope.runTest {
+                // Communal is open.
+                goToScene(CommunalScenes.Communal)
+
+                // Bouncer is visible.
+                fakeKeyguardTransitionRepository.sendTransitionSteps(
+                    KeyguardState.GLANCEABLE_HUB,
+                    KeyguardState.PRIMARY_BOUNCER,
+                    testScope
+                )
+                testableLooper.processAllMessages()
+
+                assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+            }
+        }
+
+    @Test
+    fun lifecycle_startedAfterAlternateBouncerShows() =
+        with(kosmos) {
+            testScope.runTest {
+                // Communal is open.
+                goToScene(CommunalScenes.Communal)
+
+                // Bouncer is visible.
+                fakeKeyguardTransitionRepository.sendTransitionSteps(
+                    KeyguardState.GLANCEABLE_HUB,
+                    KeyguardState.ALTERNATE_BOUNCER,
+                    testScope
+                )
+                testableLooper.processAllMessages()
+
+                assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+            }
+        }
+
+    @Test
+    fun lifecycle_createdAfterDisposeView() {
+        // Container view disposed.
+        underTest.disposeView()
+
+        assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.CREATED)
+    }
+
+    @Test
+    fun lifecycle_startedAfterShadeShows() =
+        with(kosmos) {
+            testScope.runTest {
+                // Communal is open.
+                goToScene(CommunalScenes.Communal)
+
+                // Shade shows up.
+                fakeShadeRepository.setQsExpansion(1.0f)
+                testableLooper.processAllMessages()
+
+                assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+            }
+        }
+
+    @Test
     fun editMode_communalAvailable() =
         with(kosmos) {
             testScope.runTest {
@@ -371,8 +453,6 @@
         private const val CONTAINER_WIDTH = 100
         private const val CONTAINER_HEIGHT = 100
         private const val RIGHT_SWIPE_REGION_WIDTH = 20
-        private const val TOP_SWIPE_REGION_WIDTH = 20
-        private const val BOTTOM_SWIPE_REGION_WIDTH = 20
 
         /**
          * A touch down event right in the middle of the screen, to avoid being in any of the swipe
@@ -389,17 +469,6 @@
             )
         private val DOWN_IN_RIGHT_SWIPE_REGION_EVENT =
             MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, CONTAINER_WIDTH.toFloat(), 0f, 0)
-        private val DOWN_IN_TOP_SWIPE_REGION_EVENT =
-            MotionEvent.obtain(
-                0L,
-                0L,
-                MotionEvent.ACTION_DOWN,
-                0f,
-                TOP_SWIPE_REGION_WIDTH.toFloat(),
-                0
-            )
-        private val DOWN_IN_BOTTOM_SWIPE_REGION_EVENT =
-            MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, CONTAINER_HEIGHT.toFloat(), 0)
         private val MOVE_EVENT = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_MOVE, 0f, 0f, 0)
         private val UP_EVENT = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_UP, 0f, 0f, 0)
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
index da09579..d95cc2e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
@@ -500,6 +500,46 @@
     }
 
     @Test
+    fun handleExternalTouch_intercepted_sendsOnTouch() {
+        // Accept dispatch and also intercept.
+        whenever(view.dispatchTouchEvent(any())).thenReturn(true)
+        whenever(view.onInterceptTouchEvent(any())).thenReturn(true)
+
+        underTest.handleExternalTouch(DOWN_EVENT)
+        underTest.handleExternalTouch(MOVE_EVENT)
+
+        // Once intercepted, both events are sent to the view.
+        verify(view).onTouchEvent(DOWN_EVENT)
+        verify(view).onTouchEvent(MOVE_EVENT)
+    }
+
+    @Test
+    fun handleExternalTouch_notDispatched_interceptNotCalled() {
+        // Don't accept dispatch
+        whenever(view.dispatchTouchEvent(any())).thenReturn(false)
+
+        underTest.handleExternalTouch(DOWN_EVENT)
+
+        // Interception is not offered.
+        verify(view, never()).onInterceptTouchEvent(any())
+    }
+
+    @Test
+    fun handleExternalTouch_notIntercepted_onTouchNotSent() {
+        // Accept dispatch, but don't dispatch
+        whenever(view.dispatchTouchEvent(any())).thenReturn(true)
+        whenever(view.onInterceptTouchEvent(any())).thenReturn(false)
+
+        underTest.handleExternalTouch(DOWN_EVENT)
+        underTest.handleExternalTouch(MOVE_EVENT)
+
+        // Interception offered for both events, but onTouchEvent is never called.
+        verify(view).onInterceptTouchEvent(DOWN_EVENT)
+        verify(view).onInterceptTouchEvent(MOVE_EVENT)
+        verify(view, never()).onTouchEvent(any())
+    }
+
+    @Test
     fun testGetKeyguardMessageArea() =
         testScope.runTest {
             underTest.keyguardMessageArea
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt
index 64f19b6..8eea29b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt
@@ -21,11 +21,13 @@
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.log.core.FakeLogBuffer
+import com.android.systemui.res.R
 import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository
 import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor
 import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
 import com.android.systemui.statusbar.pipeline.satellite.data.prod.FakeDeviceBasedSatelliteRepository
 import com.android.systemui.statusbar.pipeline.satellite.domain.interactor.DeviceBasedSatelliteInteractor
+import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState
 import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository
 import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractorImpl
@@ -76,7 +78,8 @@
             )
 
         underTest =
-            DeviceBasedSatelliteViewModel(
+            DeviceBasedSatelliteViewModelImpl(
+                context,
                 interactor,
                 testScope.backgroundScope,
                 airplaneModeRepository,
@@ -124,6 +127,7 @@
             assertThat(latest).isNull()
         }
 
+    @OptIn(ExperimentalCoroutinesApi::class)
     @Test
     fun icon_nullWhenShouldNotShow_isEmergencyOnly() =
         testScope.runTest {
@@ -298,4 +302,313 @@
             // THEN icon is set because the device lost wifi connection
             assertThat(latest).isInstanceOf(Icon::class.java)
         }
+
+    @Test
+    fun carrierText_nullWhenShouldNotShow_satelliteNotAllowed() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.carrierText)
+
+            // GIVEN satellite is not allowed
+            repo.isSatelliteAllowedForCurrentLocation.value = false
+
+            // GIVEN all icons are OOS
+            val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1)
+            i1.isInService.value = false
+            i1.isEmergencyOnly.value = false
+
+            // GIVEN apm is disabled
+            airplaneModeRepository.setIsAirplaneMode(false)
+
+            // THEN carrier text is null because we should not be showing it
+            assertThat(latest).isNull()
+        }
+
+    @Test
+    fun carrierText_nullWhenShouldNotShow_notAllOos() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.carrierText)
+
+            // GIVEN satellite is allowed + connected
+            repo.isSatelliteAllowedForCurrentLocation.value = true
+            repo.connectionState.value = SatelliteConnectionState.Connected
+
+            // GIVEN all icons are not OOS
+            val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1)
+            i1.isInService.value = true
+            i1.isEmergencyOnly.value = false
+
+            // GIVEN apm is disabled
+            airplaneModeRepository.setIsAirplaneMode(false)
+
+            // THEN carrier text is null because we have service
+            assertThat(latest).isNull()
+        }
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    @Test
+    fun carrierText_nullWhenShouldNotShow_isEmergencyOnly() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.carrierText)
+
+            // GIVEN satellite is allowed + connected
+            repo.isSatelliteAllowedForCurrentLocation.value = true
+            repo.connectionState.value = SatelliteConnectionState.Connected
+
+            // GIVEN all icons are OOS
+            val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1)
+            i1.isInService.value = false
+            i1.isEmergencyOnly.value = false
+
+            // GIVEN apm is disabled
+            airplaneModeRepository.setIsAirplaneMode(false)
+
+            // Wait for delay to be completed
+            advanceTimeBy(10.seconds)
+
+            // THEN carrier text is set because we don't have service
+            assertThat(latest).isNotNull()
+
+            // GIVEN the connection is emergency only
+            i1.isEmergencyOnly.value = true
+
+            // THEN carrier text is null because we have emergency connection
+            assertThat(latest).isNull()
+        }
+
+    @Test
+    fun carrierText_nullWhenShouldNotShow_apmIsEnabled() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.carrierText)
+
+            // GIVEN satellite is allowed + connected
+            repo.isSatelliteAllowedForCurrentLocation.value = true
+            repo.connectionState.value = SatelliteConnectionState.Connected
+
+            // GIVEN all icons are OOS
+            val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1)
+            i1.isInService.value = false
+            i1.isEmergencyOnly.value = false
+
+            // GIVEN apm is enabled
+            airplaneModeRepository.setIsAirplaneMode(true)
+
+            // THEN carrier text is null because we should not be showing it
+            assertThat(latest).isNull()
+        }
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    @Test
+    fun carrierText_satelliteIsOn() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.carrierText)
+
+            // GIVEN satellite is allowed + connected
+            repo.isSatelliteAllowedForCurrentLocation.value = true
+            repo.connectionState.value = SatelliteConnectionState.Connected
+
+            // GIVEN all icons are OOS
+            val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1)
+            i1.isInService.value = false
+            i1.isEmergencyOnly.value = false
+
+            // GIVEN apm is disabled
+            airplaneModeRepository.setIsAirplaneMode(false)
+
+            // Wait for delay to be completed
+            advanceTimeBy(10.seconds)
+
+            // THEN carrier text is set because we don't have service
+            assertThat(latest).isNotNull()
+        }
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    @Test
+    fun carrierText_hysteresisWhenEnablingText() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.carrierText)
+
+            // GIVEN satellite is allowed + connected
+            repo.isSatelliteAllowedForCurrentLocation.value = true
+            repo.connectionState.value = SatelliteConnectionState.Connected
+
+            // GIVEN all icons are OOS
+            val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1)
+            i1.isInService.value = false
+            i1.isEmergencyOnly.value = false
+
+            // GIVEN apm is disabled
+            airplaneModeRepository.setIsAirplaneMode(false)
+
+            // THEN carrier text is null because of the hysteresis
+            assertThat(latest).isNull()
+
+            // Wait for delay to be completed
+            advanceTimeBy(10.seconds)
+
+            // THEN carrier text is set after the delay
+            assertThat(latest).isNotNull()
+
+            // GIVEN apm is enabled
+            airplaneModeRepository.setIsAirplaneMode(true)
+
+            // THEN carrier text is null immediately
+            assertThat(latest).isNull()
+        }
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    @Test
+    fun carrierText_deviceIsProvisioned() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.carrierText)
+
+            // GIVEN satellite is allowed + connected
+            repo.isSatelliteAllowedForCurrentLocation.value = true
+            repo.connectionState.value = SatelliteConnectionState.Connected
+
+            // GIVEN all icons are OOS
+            val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1)
+            i1.isInService.value = false
+            i1.isEmergencyOnly.value = false
+
+            // GIVEN apm is disabled
+            airplaneModeRepository.setIsAirplaneMode(false)
+
+            // GIVEN device is not provisioned
+            deviceProvisionedRepository.setDeviceProvisioned(false)
+
+            // THEN carrier text is null because the device is not provisioned
+            assertThat(latest).isNull()
+
+            // GIVEN device becomes provisioned
+            deviceProvisionedRepository.setDeviceProvisioned(true)
+
+            // Wait for delay to be completed
+            advanceTimeBy(10.seconds)
+
+            // THEN carrier text is null because the device is not provisioned
+            assertThat(latest).isNotNull()
+        }
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    @Test
+    fun carrierText_wifiIsActive() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.carrierText)
+
+            // GIVEN satellite is allowed + connected
+            repo.isSatelliteAllowedForCurrentLocation.value = true
+            repo.connectionState.value = SatelliteConnectionState.Connected
+
+            // GIVEN all icons are OOS
+            val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1)
+            i1.isInService.value = false
+            i1.isEmergencyOnly.value = false
+
+            // GIVEN apm is disabled
+            airplaneModeRepository.setIsAirplaneMode(false)
+
+            // GIVEN device is provisioned
+            deviceProvisionedRepository.setDeviceProvisioned(true)
+
+            // GIVEN wifi network is active
+            wifiRepository.setWifiNetwork(WifiNetworkModel.Active(networkId = 0, level = 1))
+
+            // THEN carrier text is null because the device is connected to wifi
+            assertThat(latest).isNull()
+
+            // GIVEN device loses wifi connection
+            wifiRepository.setWifiNetwork(WifiNetworkModel.Invalid("test"))
+
+            // Wait for delay to be completed
+            advanceTimeBy(10.seconds)
+
+            // THEN carrier text is set because the device lost wifi connection
+            assertThat(latest).isNotNull()
+        }
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    @Test
+    fun carrierText_connectionStateUnknown_null() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.carrierText)
+
+            // Set up the conditions for satellite to be enabled
+            repo.isSatelliteAllowedForCurrentLocation.value = true
+            val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1)
+            i1.isInService.value = false
+            i1.isEmergencyOnly.value = false
+            airplaneModeRepository.setIsAirplaneMode(false)
+
+            repo.connectionState.value = SatelliteConnectionState.Unknown
+
+            // Wait for delay to be completed
+            advanceTimeBy(10.seconds)
+
+            assertThat(latest).isNull()
+        }
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    @Test
+    fun carrierText_connectionStateOff_null() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.carrierText)
+
+            // Set up the conditions for satellite to be enabled
+            repo.isSatelliteAllowedForCurrentLocation.value = true
+            val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1)
+            i1.isInService.value = false
+            i1.isEmergencyOnly.value = false
+            airplaneModeRepository.setIsAirplaneMode(false)
+
+            repo.connectionState.value = SatelliteConnectionState.Off
+
+            // Wait for delay to be completed
+            advanceTimeBy(10.seconds)
+
+            assertThat(latest).isNull()
+        }
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    @Test
+    fun carrierText_connectionStateOn_notConnectedString() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.carrierText)
+
+            // Set up the conditions for satellite to be enabled
+            repo.isSatelliteAllowedForCurrentLocation.value = true
+            val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1)
+            i1.isInService.value = false
+            i1.isEmergencyOnly.value = false
+            airplaneModeRepository.setIsAirplaneMode(false)
+
+            repo.connectionState.value = SatelliteConnectionState.On
+
+            // Wait for delay to be completed
+            advanceTimeBy(10.seconds)
+
+            assertThat(latest)
+                .isEqualTo(context.getString(R.string.satellite_not_connected_carrier_text))
+        }
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    @Test
+    fun carrierText_connectionStateConnected_connectedString() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.carrierText)
+
+            // Set up the conditions for satellite to be enabled
+            repo.isSatelliteAllowedForCurrentLocation.value = true
+            val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1)
+            i1.isInService.value = false
+            i1.isEmergencyOnly.value = false
+            airplaneModeRepository.setIsAirplaneMode(false)
+
+            repo.connectionState.value = SatelliteConnectionState.Connected
+
+            // Wait for delay to be completed
+            advanceTimeBy(10.seconds)
+
+            assertThat(latest)
+                .isEqualTo(context.getString(R.string.satellite_connected_carrier_text))
+        }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/FakeDeviceBasedSatelliteViewModel.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/FakeDeviceBasedSatelliteViewModel.kt
new file mode 100644
index 0000000..f125ef12
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/FakeDeviceBasedSatelliteViewModel.kt
@@ -0,0 +1,25 @@
+/*
+ * 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.statusbar.pipeline.satellite.ui.viewmodel
+
+import com.android.systemui.common.shared.model.Icon
+import kotlinx.coroutines.flow.MutableStateFlow
+
+class FakeDeviceBasedSatelliteViewModel : DeviceBasedSatelliteViewModel {
+    override val icon = MutableStateFlow<Icon?>(null)
+    override val carrierText = MutableStateFlow<String?>(null)
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFacePropertyRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFacePropertyRepository.kt
index 8a95136..65eb338 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFacePropertyRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFacePropertyRepository.kt
@@ -20,6 +20,7 @@
 import android.graphics.Point
 import com.android.systemui.biometrics.shared.model.LockoutMode
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.shared.model.DevicePosture
 import dagger.Binds
 import dagger.Module
 import javax.inject.Inject
@@ -42,6 +43,8 @@
     override val cameraInfo: StateFlow<CameraInfo?>
         get() = currentCameraInfo
 
+    override val supportedPostures: List<DevicePosture> = listOf(DevicePosture.CLOSED)
+
     fun setLockoutMode(userId: Int, mode: LockoutMode) {
         lockoutModesForUser[userId] = mode
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/SceneContainerFlagParameterization.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/SceneContainerFlagParameterization.kt
index 4e24233..e2a1fe4 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/SceneContainerFlagParameterization.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/SceneContainerFlagParameterization.kt
@@ -58,3 +58,8 @@
  */
 fun List<FlagsParameterization>.andSceneContainer(): List<FlagsParameterization> =
     flatMap { it.andSceneContainer() }.toList()
+
+/** Parameterizes only the scene container flag. */
+fun parameterizeSceneContainerFlag(): List<FlagsParameterization> {
+    return FlagsParameterization.allCombinationsOf().andSceneContainer()
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt
index 162f278..d4b7937 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt
@@ -22,6 +22,7 @@
 import android.os.fakeExecutorHandler
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.bouncer.data.repository.bouncerRepository
+import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository
 import com.android.systemui.bouncer.domain.interactor.simBouncerInteractor
 import com.android.systemui.classifier.falsingCollector
 import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
@@ -30,8 +31,8 @@
 import com.android.systemui.communal.domain.interactor.communalInteractor
 import com.android.systemui.concurrency.fakeExecutor
 import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
+import com.android.systemui.deviceentry.domain.interactor.deviceEntryUdfpsInteractor
 import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor
-import com.android.systemui.flags.fakeFeatureFlagsClassic
 import com.android.systemui.globalactions.domain.interactor.globalActionsInteractor
 import com.android.systemui.haptics.qs.qsLongPressEffect
 import com.android.systemui.jank.interactionJankMonitor
@@ -41,6 +42,7 @@
 import com.android.systemui.keyguard.domain.interactor.fromLockscreenTransitionInteractor
 import com.android.systemui.keyguard.domain.interactor.fromPrimaryBouncerTransitionInteractor
 import com.android.systemui.keyguard.domain.interactor.keyguardClockInteractor
+import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
 import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
 import com.android.systemui.model.sceneContainerPlugin
 import com.android.systemui.plugins.statusbar.statusBarStateController
@@ -50,6 +52,8 @@
 import com.android.systemui.scene.sceneContainerConfig
 import com.android.systemui.scene.shared.model.sceneDataSource
 import com.android.systemui.settings.brightness.domain.interactor.brightnessMirrorShowingInteractor
+import com.android.systemui.shade.data.repository.shadeRepository
+import com.android.systemui.shade.domain.interactor.shadeInteractor
 import com.android.systemui.shade.shadeController
 import com.android.systemui.statusbar.notification.stack.domain.interactor.sharedNotificationContainerInteractor
 import com.android.systemui.statusbar.phone.screenOffAnimationController
@@ -69,7 +73,6 @@
 
     val testDispatcher by lazy { kosmos.testDispatcher }
     val testScope by lazy { kosmos.testScope }
-    val fakeFeatureFlags by lazy { kosmos.fakeFeatureFlagsClassic }
     val fakeExecutor by lazy { kosmos.fakeExecutor }
     val fakeExecutorHandler by lazy { kosmos.fakeExecutorHandler }
     val configurationRepository by lazy { kosmos.fakeConfigurationRepository }
@@ -77,6 +80,8 @@
     val bouncerRepository by lazy { kosmos.bouncerRepository }
     val communalRepository by lazy { kosmos.fakeCommunalRepository }
     val keyguardRepository by lazy { kosmos.fakeKeyguardRepository }
+    val keyguardBouncerRepository by lazy { kosmos.fakeKeyguardBouncerRepository }
+    val keyguardInteractor by lazy { kosmos.keyguardInteractor }
     val keyguardTransitionRepository by lazy { kosmos.fakeKeyguardTransitionRepository }
     val keyguardTransitionInteractor by lazy { kosmos.keyguardTransitionInteractor }
     val powerRepository by lazy { kosmos.fakePowerRepository }
@@ -91,6 +96,7 @@
     val falsingCollector by lazy { kosmos.falsingCollector }
     val powerInteractor by lazy { kosmos.powerInteractor }
     val deviceEntryInteractor by lazy { kosmos.deviceEntryInteractor }
+    val deviceEntryUdfpsInteractor by lazy { kosmos.deviceEntryUdfpsInteractor }
     val deviceUnlockedInteractor by lazy { kosmos.deviceUnlockedInteractor }
     val communalInteractor by lazy { kosmos.communalInteractor }
     val sceneContainerPlugin by lazy { kosmos.sceneContainerPlugin }
@@ -110,6 +116,8 @@
     val brightnessMirrorShowingInteractor by lazy { kosmos.brightnessMirrorShowingInteractor }
     val qsLongPressEffect by lazy { kosmos.qsLongPressEffect }
     val shadeController by lazy { kosmos.shadeController }
+    val shadeRepository by lazy { kosmos.shadeRepository }
+    val shadeInteractor by lazy { kosmos.shadeInteractor }
 
     init {
         kosmos.applicationContext = testCase.context
diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/FakeWindowRootViewComponent.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/FakeWindowRootViewComponent.kt
similarity index 95%
rename from packages/SystemUI/tests/src/com/android/systemui/scene/FakeWindowRootViewComponent.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/scene/FakeWindowRootViewComponent.kt
index 63a05d7..6ac3a62 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/scene/FakeWindowRootViewComponent.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/FakeWindowRootViewComponent.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * 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.
diff --git a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
index 5e566aa..522aa67 100644
--- a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
+++ b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
@@ -202,6 +202,7 @@
     // Default max API calls per reset interval for generated preview API rate limiting.
     private static final int DEFAULT_GENERATED_PREVIEW_MAX_CALLS_PER_INTERVAL = 2;
 
+    private static final String PENDING_DELETED_IDS_ATTR = "pending_deleted_ids";
 
     private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
         @Override
@@ -228,6 +229,11 @@
                     onPackageBroadcastReceived(intent, getSendingUserId());
                     updateWidgetPackageSuspensionMaskedState(intent, false, getSendingUserId());
                     break;
+                case Intent.ACTION_PACKAGE_RESTARTED:
+                case Intent.ACTION_PACKAGE_UNSTOPPED:
+                    if (!android.content.pm.Flags.stayStopped()) return;
+                    updateWidgetPackageStoppedMaskedState(intent);
+                    break;
                 default:
                     onPackageBroadcastReceived(intent, getSendingUserId());
                     break;
@@ -396,7 +402,10 @@
         packageFilter.addAction(Intent.ACTION_PACKAGE_CHANGED);
         packageFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
         packageFilter.addAction(Intent.ACTION_PACKAGE_DATA_CLEARED);
+        packageFilter.addAction(Intent.ACTION_PACKAGE_RESTARTED);
+        packageFilter.addAction(Intent.ACTION_PACKAGE_UNSTOPPED);
         packageFilter.addDataScheme("package");
+        packageFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
         mContext.registerReceiverAsUser(mBroadcastReceiver, UserHandle.ALL,
                 packageFilter, null, mCallbackHandler);
 
@@ -581,14 +590,19 @@
                 changed |= provider.setMaskedByQuietProfileLocked(quietProfile);
                 try {
                     boolean suspended;
+                    boolean stopped;
                     try {
                         suspended = mPackageManager.isPackageSuspendedForUser(
                                 provider.id.componentName.getPackageName(), provider.getUserId());
+                        stopped = mPackageManager.isPackageStoppedForUser(
+                                provider.id.componentName.getPackageName(), provider.getUserId());
                     } catch (IllegalArgumentException ex) {
                         // Package not found.
                         suspended = false;
+                        stopped = false;
                     }
                     changed |= provider.setMaskedBySuspendedPackageLocked(suspended);
+                    changed |= provider.setMaskedByStoppedPackageLocked(stopped);
                 } catch (RemoteException e) {
                     Slog.e(TAG, "Failed to query application info", e);
                 }
@@ -636,6 +650,82 @@
     }
 
     /**
+     * Update the masked state for a stopped or unstopped package.
+     */
+    private void updateWidgetPackageStoppedMaskedState(@NonNull Intent intent) {
+        final int providerUid = intent.getIntExtra(Intent.EXTRA_UID, Process.INVALID_UID);
+        final Uri uri = intent.getData();
+        if (providerUid == Process.INVALID_UID || uri == null) {
+            return;
+        }
+
+        final String packageName = uri.getSchemeSpecificPart();
+        if (packageName == null) {
+            return;
+        }
+
+        boolean isStopped;
+        try {
+            isStopped = mPackageManager.isPackageStoppedForUser(packageName,
+                    UserHandle.getUserId(providerUid));
+        } catch (Exception e) {
+            Slog.e(TAG, "Failed to query package stopped state", e);
+            return;
+        }
+
+        if (DEBUG) {
+            Slog.i(TAG, "Updating package stopped masked state for uid " + providerUid + " package "
+                    + packageName + " isStopped " + isStopped);
+        }
+        synchronized (mLock) {
+            final int count = mProviders.size();
+            for (int i = 0; i < count; i++) {
+                Provider provider = mProviders.get(i);
+                if (providerUid != provider.id.uid
+                        || !packageName.equals(provider.id.componentName.getPackageName())) {
+                    continue;
+                }
+                if (provider.setMaskedByStoppedPackageLocked(isStopped)) {
+                    if (provider.isMaskedLocked()) {
+                        maskWidgetsViewsLocked(provider, null);
+                        cancelBroadcastsLocked(provider);
+                    } else {
+                        unmaskWidgetsViewsLocked(provider);
+                        final int widgetCount = provider.widgets.size();
+                        if (widgetCount > 0) {
+                            final int[] widgetIds = new int[widgetCount];
+                            for (int j = 0; j < widgetCount; j++) {
+                                widgetIds[j] = provider.widgets.get(j).appWidgetId;
+                            }
+                            registerForBroadcastsLocked(provider, widgetIds);
+                            sendUpdateIntentLocked(provider, widgetIds, /* interactive= */ false);
+                        }
+
+                        final int pendingIdsCount = provider.pendingDeletedWidgetIds.size();
+                        if (pendingIdsCount > 0) {
+                            if (DEBUG) {
+                                Slog.i(TAG, "Sending missed deleted broadcasts for "
+                                        + provider.id.componentName + " "
+                                        + provider.pendingDeletedWidgetIds);
+                            }
+                            for (int j = 0; j < pendingIdsCount; j++) {
+                                sendDeletedIntentLocked(provider.id.componentName,
+                                        provider.id.getProfile(),
+                                        provider.pendingDeletedWidgetIds.get(j));
+                            }
+                            provider.pendingDeletedWidgetIds.clear();
+                            if (widgetCount == 0) {
+                                sendDisabledIntentLocked(provider);
+                            }
+                            saveGroupStateAsync(provider.id.getProfile().getIdentifier());
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    /**
      * Mask the target widget belonging to the specified provider, or all active widgets
      * of the provider if target widget == null.
      */
@@ -648,11 +738,11 @@
                 R.layout.work_widget_mask_view);
         ApplicationInfo appInfo = provider.info.providerInfo.applicationInfo;
         final int appUserId = provider.getUserId();
-        boolean showBadge;
+        boolean showBadge = false;
 
         final long identity = Binder.clearCallingIdentity();
         try {
-            final Intent onClickIntent;
+            Intent onClickIntent = null;
 
             if (provider.maskedByQuietProfile) {
                 showBadge = true;
@@ -676,7 +766,7 @@
                             appInfo.packageName, suspendingPackage, dialogInfo, null, null,
                             appUserId);
                 }
-            } else /* provider.maskedByLockedProfile */ {
+            } else if (provider.maskedByLockedProfile) {
                 showBadge = true;
                 onClickIntent = mKeyguardManager
                         .createConfirmDeviceCredentialIntent(null, null, appUserId);
@@ -684,6 +774,8 @@
                     onClickIntent.setFlags(
                             FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
                 }
+            } else if (provider.maskedByStoppedPackage) {
+                showBadge = mUserManager.hasBadge(appUserId);
             }
 
             Icon icon = appInfo.icon != 0
@@ -697,7 +789,14 @@
             for (int j = 0; j < widgetCount; j++) {
                 Widget widget = provider.widgets.get(j);
                 if (targetWidget != null && targetWidget != widget) continue;
-                if (onClickIntent != null) {
+                if (provider.maskedByStoppedPackage) {
+                    Intent intent = createUpdateIntentLocked(provider,
+                            new int[] { widget.appWidgetId });
+                    views.setOnClickPendingIntent(android.R.id.background,
+                            PendingIntent.getBroadcast(mContext, widget.appWidgetId,
+                                    intent, PendingIntent.FLAG_UPDATE_CURRENT
+                                            | PendingIntent.FLAG_IMMUTABLE));
+                } else if (onClickIntent != null) {
                     views.setOnClickPendingIntent(android.R.id.background,
                             PendingIntent.getActivity(mContext, widget.appWidgetId, onClickIntent,
                                     PendingIntent.FLAG_UPDATE_CURRENT
@@ -1950,15 +2049,23 @@
         if (provider != null) {
             provider.widgets.remove(widget);
             if (!provider.zombie) {
-                // send the broacast saying that this appWidgetId has been deleted
-                sendDeletedIntentLocked(widget);
+                // If the package is not stopped, send the broadcast saying that this appWidgetId
+                // has been deleted. Otherwise, save the ID and send the broadcast when the package
+                // is unstopped.
+                if (!provider.maskedByStoppedPackage) {
+                    sendDeletedIntentLocked(widget);
+                } else {
+                    provider.pendingDeletedWidgetIds.add(widget.appWidgetId);
+                }
 
                 if (provider.widgets.isEmpty()) {
                     // cancel the future updates
                     cancelBroadcastsLocked(provider);
 
-                    // send the broacast saying that the provider is not in use any more
-                    sendDisabledIntentLocked(provider);
+                    // send the broadcast saying that the provider is not in use any more
+                    if (!provider.maskedByStoppedPackage) {
+                        sendDisabledIntentLocked(provider);
+                    }
                 }
             }
         }
@@ -2033,8 +2140,9 @@
             final HashSet<Integer> ids = mRemoteViewsServicesAppWidgets.get(key);
             if (ids.remove(widget.appWidgetId)) {
                 // If we have removed the last app widget referencing this service, then we
-                // should destroy it and remove it from this set
-                if (ids.isEmpty()) {
+                // should destroy it and remove it from this set. This is skipped for widgets whose
+                // provider is in a stopped package, to avoid waking up the package.
+                if (ids.isEmpty() && !widget.provider.maskedByStoppedPackage) {
                     destroyRemoteViewsService(key.second.getIntent(), widget);
                     it.remove();
                 }
@@ -2544,18 +2652,29 @@
 
     private void sendUpdateIntentLocked(Provider provider, int[] appWidgetIds,
             boolean interactive) {
-        Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
-        intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds);
-        intent.setComponent(provider.id.componentName);
+        Intent intent = createUpdateIntentLocked(provider, appWidgetIds);
         sendBroadcastAsUser(intent, provider.id.getProfile(), interactive);
     }
 
+    private Intent createUpdateIntentLocked(Provider provider, int[] appWidgetIds) {
+        Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
+        intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds);
+        intent.setComponent(provider.id.componentName);
+        return intent;
+    }
+
     private void sendDeletedIntentLocked(Widget widget) {
+        sendDeletedIntentLocked(widget.provider.id.componentName, widget.provider.id.getProfile(),
+                widget.appWidgetId);
+    }
+
+    private void sendDeletedIntentLocked(ComponentName provider, UserHandle profile,
+            int appWidgetId) {
         Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_DELETED);
-        intent.setComponent(widget.provider.id.componentName);
-        intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widget.appWidgetId);
+        intent.setComponent(provider);
+        intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
         // Cleanup after deletion isn't an interactive UX case
-        sendBroadcastAsUser(intent, widget.provider.id.getProfile(), false);
+        sendBroadcastAsUser(intent, profile, false);
     }
 
     private void sendDisabledIntentLocked(Provider provider) {
@@ -2684,6 +2803,14 @@
         if (persistsProviderInfo && p.mInfoParsed) {
             AppWidgetXmlUtil.writeAppWidgetProviderInfoLocked(out, p.info);
         }
+        final int pendingIdsCount = p.pendingDeletedWidgetIds.size();
+        if (pendingIdsCount > 0) {
+            final List<String> idStrings = new ArrayList<>();
+            for (int i = 0; i < pendingIdsCount; i++) {
+                idStrings.add(String.valueOf(p.pendingDeletedWidgetIds.get(i)));
+            }
+            out.attribute(null, PENDING_DELETED_IDS_ATTR, String.join(",", idStrings));
+        }
         out.endTag(null, "p");
     }
 
@@ -3022,7 +3149,7 @@
                     continue;
                 }
 
-                if (provider.widgets.size() > 0) {
+                if (provider.widgets.size() > 0 && !provider.maskedByStoppedPackage) {
                     Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER,
                             "appwidget init " + provider.id.componentName.getPackageName());
                     provider.widgets.forEach(widget -> {
@@ -3440,6 +3567,16 @@
                                 legacyProviderIndex);
                         provider.tag = providerTag;
                         provider.infoTag = parser.getAttributeValue(null, "info_tag");
+
+                        final String pendingDeletedIds = parser.getAttributeValue(null,
+                                PENDING_DELETED_IDS_ATTR);
+                        if (pendingDeletedIds != null && !pendingDeletedIds.isEmpty()) {
+                            final String[] idStrings = pendingDeletedIds.split(",");
+                            for (int i = 0; i < idStrings.length; i++) {
+                                provider.pendingDeletedWidgetIds.add(
+                                        Integer.parseInt(idStrings[i]));
+                            }
+                        }
                     } else if ("h".equals(tag)) {
                         legacyHostIndex++;
                         Host host = new Host();
@@ -4443,6 +4580,11 @@
         boolean maskedByLockedProfile;
         boolean maskedByQuietProfile;
         boolean maskedBySuspendedPackage;
+        // This provider's package has been stopped
+        boolean maskedByStoppedPackage;
+        // Widget IDs for which we haven't yet sent DELETED broadcasts because the package was
+        // stopped.
+        IntArray pendingDeletedWidgetIds = new IntArray();
 
         boolean mInfoParsed = false;
 
@@ -4598,8 +4740,15 @@
             return masked != oldState;
         }
 
+        public boolean setMaskedByStoppedPackageLocked(boolean masked) {
+            boolean oldState = maskedByStoppedPackage;
+            maskedByStoppedPackage = masked;
+            return masked != oldState;
+        }
+
         public boolean isMaskedLocked() {
-            return maskedByQuietProfile || maskedByLockedProfile || maskedBySuspendedPackage;
+            return maskedByQuietProfile || maskedByLockedProfile || maskedBySuspendedPackage
+                    || maskedByStoppedPackage;
         }
 
         public boolean shouldBePersisted() {
diff --git a/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java b/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java
index 6c7546e..64bca33 100644
--- a/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java
+++ b/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java
@@ -17,6 +17,7 @@
 package com.android.server;
 
 import static android.permission.flags.Flags.sensitiveContentImprovements;
+import static android.permission.flags.Flags.sensitiveContentMetricsBugfix;
 import static android.permission.flags.Flags.sensitiveNotificationAppProtection;
 import static android.provider.Settings.Global.DISABLE_SCREEN_SHARE_PROTECTIONS_FOR_APPS_AND_NOTIFICATIONS;
 import static android.view.flags.Flags.sensitiveContentAppProtection;
@@ -93,7 +94,7 @@
     private boolean mProjectionActive = false;
 
     private static class MediaProjectionSession {
-        private final int mUid;
+        private final int mUid; // UID of app that started projection session
         private final long mSessionId;
         private final boolean mIsExempted;
         private final ArraySet<String> mAllSeenNotificationKeys = new ArraySet<>();
@@ -320,6 +321,12 @@
             }
 
             mProjectionActive = true;
+
+            if (sensitiveContentMetricsBugfix()) {
+                mWindowManager.setBlockScreenCaptureForAppsSessionId(
+                        mMediaProjectionSession.mSessionId);
+            }
+
             if (sensitiveNotificationAppProtection()) {
                 updateAppsThatShouldBlockScreenCapture();
             }
diff --git a/services/core/java/com/android/server/TelephonyRegistry.java b/services/core/java/com/android/server/TelephonyRegistry.java
index bd67cf420..e171064 100644
--- a/services/core/java/com/android/server/TelephonyRegistry.java
+++ b/services/core/java/com/android/server/TelephonyRegistry.java
@@ -425,6 +425,8 @@
     private int[] mSCBMReason;
     private boolean[] mSCBMStarted;
 
+    private boolean[] mCarrierRoamingNtnMode = null;
+
     /**
      * Per-phone map of precise data connection state. The key of the map is the pair of transport
      * type and APN setting. This is the cache to prevent redundant callbacks to the listeners.
@@ -723,6 +725,7 @@
             mECBMStarted = copyOf(mECBMStarted, mNumPhones);
             mSCBMReason = copyOf(mSCBMReason, mNumPhones);
             mSCBMStarted = copyOf(mSCBMStarted, mNumPhones);
+            mCarrierRoamingNtnMode = copyOf(mCarrierRoamingNtnMode, mNumPhones);
             // ds -> ss switch.
             if (mNumPhones < oldNumPhones) {
                 cutListToSize(mCellInfo, mNumPhones);
@@ -781,6 +784,7 @@
                 mECBMStarted[i] = false;
                 mSCBMReason[i] = TelephonyManager.STOP_REASON_UNKNOWN;
                 mSCBMStarted[i] = false;
+                mCarrierRoamingNtnMode[i] = false;
             }
         }
     }
@@ -854,6 +858,7 @@
         mECBMStarted = new boolean[numPhones];
         mSCBMReason = new int[numPhones];
         mSCBMStarted = new boolean[numPhones];
+        mCarrierRoamingNtnMode = new boolean[numPhones];
 
         for (int i = 0; i < numPhones; i++) {
             mCallState[i] =  TelephonyManager.CALL_STATE_IDLE;
@@ -897,6 +902,7 @@
             mECBMStarted[i] = false;
             mSCBMReason[i] = TelephonyManager.STOP_REASON_UNKNOWN;
             mSCBMStarted[i] = false;
+            mCarrierRoamingNtnMode[i] = false;
         }
 
         mAppOps = mContext.getSystemService(AppOpsManager.class);
@@ -1523,6 +1529,14 @@
                         remove(r.binder);
                     }
                 }
+                if (events.contains(TelephonyCallback.EVENT_CARRIER_ROAMING_NTN_MODE_CHANGED)) {
+                    try {
+                        r.callback.onCarrierRoamingNtnModeChanged(
+                                mCarrierRoamingNtnMode[r.phoneId]);
+                    } catch (RemoteException ex) {
+                        remove(r.binder);
+                    }
+                }
             }
         }
     }
@@ -3512,6 +3526,41 @@
         handleRemoveListLocked();
     }
 
+    /**
+     * Notify external listeners that carrier roaming non-terrestrial network mode changed.
+     * @param subId subscription ID.
+     * @param active {@code true} If the device is connected to carrier roaming
+     *                           non-terrestrial network or was connected within the
+     *                           {CarrierConfigManager#KEY_SATELLITE_CONNECTION_HYSTERESIS_SEC_INT}
+     *                           duration, {code false} otherwise.
+     */
+    public void notifyCarrierRoamingNtnModeChanged(int subId, boolean active) {
+        if (!checkNotifyPermission("notifyCarrierRoamingNtnModeChanged")) {
+            return;
+        }
+
+        if (VDBG) {
+            log("notifyCarrierRoamingNtnModeChanged: subId=" + subId + " active=" + active);
+        }
+
+        synchronized (mRecords) {
+            int phoneId = getPhoneIdFromSubId(subId);
+            mCarrierRoamingNtnMode[phoneId] = active;
+            for (Record r : mRecords) {
+                if (r.matchTelephonyCallbackEvent(
+                        TelephonyCallback.EVENT_CARRIER_ROAMING_NTN_MODE_CHANGED)
+                        && idMatch(r, subId, phoneId)) {
+                    try {
+                        r.callback.onCarrierRoamingNtnModeChanged(active);
+                    } catch (RemoteException ex) {
+                        mRemoveList.add(r.binder);
+                    }
+                }
+            }
+            handleRemoveListLocked();
+        }
+    }
+
     @NeverCompile // Avoid size overhead of debugging code.
     @Override
     public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index c3a3bf7..eec22c9 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -2422,8 +2422,6 @@
                                     getTimeLimitedFgsType(foregroundServiceType);
                             final TimeLimitedFgsInfo fgsTypeInfo = fgsInfo.get(timeLimitedFgsType);
                             if (fgsTypeInfo != null) {
-                                // TODO(b/330399444): check to see if all time book-keeping for
-                                //  time limited types should use elapsedRealtime instead of uptime
                                 final long before24Hr = Math.max(0,
                                             SystemClock.elapsedRealtime() - (24 * 60 * 60 * 1000));
                                 final long lastTimeOutAt = fgsTypeInfo.getTimeLimitExceededAt();
@@ -3757,7 +3755,7 @@
         }
 
         traceInstant("FGS start: ", sr);
-        final long nowUptime = SystemClock.uptimeMillis();
+        final long nowRealtime = SystemClock.elapsedRealtime();
 
         // Fetch/create/update the fgs info for the time-limited type.
         SparseArray<TimeLimitedFgsInfo> fgsInfo = mTimeLimitedFgsInfo.get(sr.appInfo.uid);
@@ -3768,10 +3766,10 @@
         final int timeLimitedFgsType = getTimeLimitedFgsType(sr.foregroundServiceType);
         TimeLimitedFgsInfo fgsTypeInfo = fgsInfo.get(timeLimitedFgsType);
         if (fgsTypeInfo == null) {
-            fgsTypeInfo = sr.createTimeLimitedFgsInfo(nowUptime);
+            fgsTypeInfo = sr.createTimeLimitedFgsInfo(nowRealtime);
             fgsInfo.put(timeLimitedFgsType, fgsTypeInfo);
         }
-        fgsTypeInfo.setLastFgsStartTime(nowUptime);
+        fgsTypeInfo.setLastFgsStartTime(nowRealtime);
 
         // We'll cancel the previous ANR timer and start a fresh one below.
         mFGSAnrTimer.cancel(sr);
@@ -3845,14 +3843,14 @@
                 final TimeLimitedFgsInfo fgsTypeInfo = fgsInfo.get(fgsType);
                 if (fgsTypeInfo != null) {
                     // Update total runtime for the time-limited fgs type and mark it as timed out.
-                    final long nowUptime = SystemClock.uptimeMillis();
+                    final long nowRealtime = SystemClock.elapsedRealtime();
                     fgsTypeInfo.updateTotalRuntime();
-                    fgsTypeInfo.setTimeLimitExceededAt(nowUptime);
+                    fgsTypeInfo.setTimeLimitExceededAt(nowRealtime);
 
                     logFGSStateChangeLocked(sr,
                             FOREGROUND_SERVICE_STATE_CHANGED__STATE__TIMED_OUT,
-                            nowUptime > fgsTypeInfo.getLastFgsStartTime()
-                                    ? (int) (nowUptime - fgsTypeInfo.getLastFgsStartTime()) : 0,
+                            nowRealtime > fgsTypeInfo.getLastFgsStartTime()
+                                    ? (int) (nowRealtime - fgsTypeInfo.getLastFgsStartTime()) : 0,
                             FGS_STOP_REASON_UNKNOWN,
                             FGS_TYPE_POLICY_CHECK_UNKNOWN,
                             FOREGROUND_SERVICE_STATE_CHANGED__FGS_START_API__FGSSTARTAPI_NA,
diff --git a/services/core/java/com/android/server/am/ServiceRecord.java b/services/core/java/com/android/server/am/ServiceRecord.java
index 045d137..065f3bd 100644
--- a/services/core/java/com/android/server/am/ServiceRecord.java
+++ b/services/core/java/com/android/server/am/ServiceRecord.java
@@ -30,9 +30,9 @@
 import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM;
 import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME;
 
+import android.annotation.ElapsedRealtimeLong;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.annotation.UptimeMillisLong;
 import android.app.BackgroundStartPrivileges;
 import android.app.IApplicationThread;
 import android.app.Notification;
@@ -677,46 +677,46 @@
      * Data container class to help track certain fgs info for time-restricted types.
      */
     static class TimeLimitedFgsInfo {
-        @UptimeMillisLong
+        @ElapsedRealtimeLong
         private long mFirstFgsStartTime;
-        @UptimeMillisLong
+        @ElapsedRealtimeLong
         private long mLastFgsStartTime;
-        @UptimeMillisLong
+        @ElapsedRealtimeLong
         private long mTimeLimitExceededAt = Long.MIN_VALUE;
         private long mTotalRuntime = 0;
 
-        TimeLimitedFgsInfo(@UptimeMillisLong long startTime) {
+        TimeLimitedFgsInfo(@ElapsedRealtimeLong long startTime) {
             mFirstFgsStartTime = startTime;
             mLastFgsStartTime = startTime;
         }
 
-        @UptimeMillisLong
+        @ElapsedRealtimeLong
         public long getFirstFgsStartTime() {
             return mFirstFgsStartTime;
         }
 
-        public void setLastFgsStartTime(@UptimeMillisLong long startTime) {
+        public void setLastFgsStartTime(@ElapsedRealtimeLong long startTime) {
             mLastFgsStartTime = startTime;
         }
 
-        @UptimeMillisLong
+        @ElapsedRealtimeLong
         public long getLastFgsStartTime() {
             return mLastFgsStartTime;
         }
 
         public void updateTotalRuntime() {
-            mTotalRuntime += SystemClock.uptimeMillis() - mLastFgsStartTime;
+            mTotalRuntime += SystemClock.elapsedRealtime() - mLastFgsStartTime;
         }
 
         public long getTotalRuntime() {
             return mTotalRuntime;
         }
 
-        public void setTimeLimitExceededAt(@UptimeMillisLong long timeLimitExceededAt) {
+        public void setTimeLimitExceededAt(@ElapsedRealtimeLong long timeLimitExceededAt) {
             mTimeLimitExceededAt = timeLimitExceededAt;
         }
 
-        @UptimeMillisLong
+        @ElapsedRealtimeLong
         public long getTimeLimitExceededAt() {
             return mTimeLimitExceededAt;
         }
@@ -1858,8 +1858,8 @@
     /**
      * Called when a time-limited FGS starts.
      */
-    public TimeLimitedFgsInfo createTimeLimitedFgsInfo(long nowUptime) {
-        return new TimeLimitedFgsInfo(nowUptime);
+    public TimeLimitedFgsInfo createTimeLimitedFgsInfo(@ElapsedRealtimeLong long nowRealtime) {
+        return new TimeLimitedFgsInfo(nowRealtime);
     }
 
     /**
diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java
index b8bfeda..e59de6a 100644
--- a/services/core/java/com/android/server/appop/AppOpsService.java
+++ b/services/core/java/com/android/server/appop/AppOpsService.java
@@ -2773,15 +2773,15 @@
             }
             code = AppOpsManager.opToSwitch(code);
             UidState uidState = getUidStateLocked(uid, false);
-            if (uidState != null
-                    && mAppOpsCheckingService.getUidMode(
-                                    uidState.uid, getPersistentId(virtualDeviceId), code)
-                            != AppOpsManager.opToDefaultMode(code)) {
-                final int rawMode =
-                        mAppOpsCheckingService.getUidMode(
-                                uidState.uid, getPersistentId(virtualDeviceId), code);
-                return raw ? rawMode : uidState.evalMode(code, rawMode);
+            if (uidState != null) {
+                int rawUidMode = mAppOpsCheckingService.getUidMode(
+                        uidState.uid, getPersistentId(virtualDeviceId), code);
+
+                if (rawUidMode != AppOpsManager.opToDefaultMode(code)) {
+                    return raw ? rawUidMode : uidState.evalMode(code, rawUidMode);
+                }
             }
+
             Op op = getOpLocked(code, uid, packageName, null, false, pvr.bypass, /* edit */ false);
             if (op == null) {
                 return AppOpsManager.opToDefaultMode(code);
@@ -3682,26 +3682,24 @@
             isRestricted = isOpRestrictedLocked(uid, code, packageName, attributionTag,
                     virtualDeviceId, pvr.bypass, false);
             final int switchCode = AppOpsManager.opToSwitch(code);
+
+            int rawUidMode;
             if (isOpAllowedForUid(uid)) {
                 // Op is always allowed for the UID, do nothing.
 
                 // If there is a non-default per UID policy (we set UID op mode only if
                 // non-default) it takes over, otherwise use the per package policy.
-            } else if (mAppOpsCheckingService.getUidMode(
-                    uidState.uid, getPersistentId(virtualDeviceId), switchCode)
+            } else if ((rawUidMode =
+                            mAppOpsCheckingService.getUidMode(
+                                    uidState.uid, getPersistentId(virtualDeviceId), switchCode))
                     != AppOpsManager.opToDefaultMode(switchCode)) {
-                final int uidMode =
-                        uidState.evalMode(
-                                code,
-                                mAppOpsCheckingService.getUidMode(
-                                        uidState.uid,
-                                        getPersistentId(virtualDeviceId),
-                                        switchCode));
+                final int uidMode = uidState.evalMode(code, rawUidMode);
                 if (!shouldStartForMode(uidMode, startIfModeDefault)) {
                     if (DEBUG) {
                         Slog.d(TAG, "startOperation: uid reject #" + uidMode + " for code "
                                 + switchCode + " (" + code + ") uid " + uid + " package "
-                                + packageName + " flags: " + AppOpsManager.flagsToString(flags));
+                                + packageName + " flags: "
+                                + AppOpsManager.flagsToString(flags));
                     }
                     attributedOp.rejected(uidState.getState(), flags);
                     scheduleOpStartedIfNeededLocked(code, uid, packageName, attributionTag,
@@ -3710,8 +3708,8 @@
                     return new SyncNotedAppOp(uidMode, code, attributionTag, packageName);
                 }
             } else {
-                final Op switchOp = switchCode != code ? getOpLocked(ops, switchCode, uid, true)
-                        : op;
+                final Op switchOp =
+                        switchCode != code ? getOpLocked(ops, switchCode, uid, true) : op;
                 final int mode =
                         switchOp.uidState.evalMode(
                                 switchOp.op,
@@ -3721,9 +3719,12 @@
                                         UserHandle.getUserId(switchOp.uid)));
                 if (mode != AppOpsManager.MODE_ALLOWED
                         && (!startIfModeDefault || mode != MODE_DEFAULT)) {
-                    if (DEBUG) Slog.d(TAG, "startOperation: reject #" + mode + " for code "
-                            + switchCode + " (" + code + ") uid " + uid + " package "
-                            + packageName + " flags: " + AppOpsManager.flagsToString(flags));
+                    if (DEBUG) {
+                        Slog.d(TAG, "startOperation: reject #" + mode + " for code "
+                                + switchCode + " (" + code + ") uid " + uid + " package "
+                                + packageName + " flags: "
+                                + AppOpsManager.flagsToString(flags));
+                    }
                     attributedOp.rejected(uidState.getState(), flags);
                     scheduleOpStartedIfNeededLocked(code, uid, packageName, attributionTag,
                             virtualDeviceId, flags, mode, startType, attributionFlags,
@@ -3731,6 +3732,7 @@
                     return new SyncNotedAppOp(mode, code, attributionTag, packageName);
                 }
             }
+
             if (DEBUG) Slog.d(TAG, "startOperation: allowing code " + code + " uid " + uid
                     + " package " + packageName + " restricted: " + isRestricted
                     + " flags: " + AppOpsManager.flagsToString(flags));
diff --git a/services/core/java/com/android/server/display/BrightnessRangeController.java b/services/core/java/com/android/server/display/BrightnessRangeController.java
index dc0e80c..9b37418 100644
--- a/services/core/java/com/android/server/display/BrightnessRangeController.java
+++ b/services/core/java/com/android/server/display/BrightnessRangeController.java
@@ -19,7 +19,6 @@
 import android.hardware.display.BrightnessInfo;
 import android.os.Handler;
 import android.os.IBinder;
-import android.os.PowerManager;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.display.brightness.clamper.HdrClamper;
@@ -148,9 +147,7 @@
 
     float getHdrBrightnessValue() {
         float hdrBrightness = mHbmController.getHdrBrightnessValue();
-        float brightnessMax = mUseHdrClamper ? mHdrClamper.getMaxBrightness()
-                : PowerManager.BRIGHTNESS_MAX;
-        return Math.min(hdrBrightness, brightnessMax);
+        return mUseHdrClamper ? mHdrClamper.clamp(hdrBrightness) : hdrBrightness;
     }
 
     float getTransitionPoint() {
diff --git a/services/core/java/com/android/server/display/brightness/clamper/HdrClamper.java b/services/core/java/com/android/server/display/brightness/clamper/HdrClamper.java
index f1cb66c..902daa4 100644
--- a/services/core/java/com/android/server/display/brightness/clamper/HdrClamper.java
+++ b/services/core/java/com/android/server/display/brightness/clamper/HdrClamper.java
@@ -59,6 +59,11 @@
 
     private boolean mAutoBrightnessEnabled = false;
 
+    /**
+     * Indicates that maxBrightness is changed, and we should use slow transition
+     */
+    private boolean mUseSlowTransition = false;
+
     public HdrClamper(BrightnessClamperController.ClamperChangeListener clamperChangeListener,
             Handler handler) {
         this(clamperChangeListener, handler, new Injector());
@@ -72,6 +77,7 @@
         mDebouncer = () -> {
             mTransitionRate = mDesiredTransitionRate;
             mMaxBrightness = mDesiredMaxBrightness;
+            mUseSlowTransition = true;
             mClamperChangeListener.onChanged();
         };
         mHdrListener = injector.getHdrListener((visible) -> {
@@ -80,14 +86,24 @@
         }, handler);
     }
 
-    // Called in same looper: mHandler.getLooper()
+    /**
+     * Applies clamping
+     * Called in same looper: mHandler.getLooper()
+     */
+    public float clamp(float brightness) {
+        return Math.min(brightness, mMaxBrightness);
+    }
+
+    @VisibleForTesting
     public float getMaxBrightness() {
         return mMaxBrightness;
     }
 
     // Called in same looper: mHandler.getLooper()
     public float getTransitionRate() {
-        return mTransitionRate;
+        float expectedTransitionRate =  mUseSlowTransition ? mTransitionRate : -1;
+        mUseSlowTransition = false;
+        return  expectedTransitionRate;
     }
 
     /**
@@ -173,7 +189,8 @@
         mMaxBrightness = PowerManager.BRIGHTNESS_MAX;
         mDesiredMaxBrightness = PowerManager.BRIGHTNESS_MAX;
         mDesiredTransitionRate = -1f;
-        mTransitionRate = 1f;
+        mTransitionRate = -1f;
+        mUseSlowTransition = false;
         mClamperChangeListener.onChanged();
     }
 
diff --git a/services/core/java/com/android/server/dreams/DreamController.java b/services/core/java/com/android/server/dreams/DreamController.java
index 0e8a5fb..a818eab 100644
--- a/services/core/java/com/android/server/dreams/DreamController.java
+++ b/services/core/java/com/android/server/dreams/DreamController.java
@@ -249,6 +249,16 @@
         mCurrentDream.mAppTask = appTask;
     }
 
+    void setDreamHasFocus(boolean hasFocus) {
+        if (mCurrentDream != null) {
+            mCurrentDream.mDreamHasFocus = hasFocus;
+        }
+    }
+
+    boolean dreamHasFocus() {
+        return mCurrentDream != null && mCurrentDream.mDreamHasFocus;
+    }
+
     /**
      * Sends a user activity signal to PowerManager to stop the screen from turning off immediately
      * if there hasn't been any user interaction in a while.
@@ -271,6 +281,21 @@
         stopDreamInstance(immediate, reason, mCurrentDream);
     }
 
+    public boolean bringDreamToFront() {
+        if (mCurrentDream == null || mCurrentDream.mService == null) {
+            return false;
+        }
+
+        try {
+            mCurrentDream.mService.comeToFront();
+            return true;
+        } catch (RemoteException e) {
+            Slog.e(TAG, "Error asking dream to come to the front", e);
+        }
+
+        return false;
+    }
+
     /**
      * Stops the given dream instance.
      *
@@ -426,6 +451,7 @@
         private String mStopReason;
         private long mDreamStartTime;
         public boolean mWakingGently;
+        public boolean mDreamHasFocus;
 
         private final Runnable mStopPreviousDreamsIfNeeded = this::stopPreviousDreamsIfNeeded;
         private final Runnable mReleaseWakeLockIfNeeded = this::releaseWakeLockIfNeeded;
diff --git a/services/core/java/com/android/server/dreams/DreamManagerService.java b/services/core/java/com/android/server/dreams/DreamManagerService.java
index 42c9e08..fc63494 100644
--- a/services/core/java/com/android/server/dreams/DreamManagerService.java
+++ b/services/core/java/com/android/server/dreams/DreamManagerService.java
@@ -20,6 +20,7 @@
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_ASSISTANT;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_DREAM;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+import static android.service.dreams.Flags.dreamTracksFocus;
 
 import static com.android.server.wm.ActivityInterceptorCallback.DREAM_MANAGER_ORDERED_ID;
 
@@ -406,8 +407,10 @@
     /** Whether dreaming can start given user settings and the current dock/charge state. */
     private boolean canStartDreamingInternal(boolean isScreenOn) {
         synchronized (mLock) {
-            // Can't start dreaming if we are already dreaming.
-            if (isScreenOn && isDreamingInternal()) {
+            // Can't start dreaming if we are already dreaming and the dream has focus. If we are
+            // dreaming but the dream does not have focus, then the dream can be brought to the
+            // front so it does have focus.
+            if (isScreenOn && isDreamingInternal() && dreamHasFocus()) {
                 return false;
             }
 
@@ -442,11 +445,20 @@
         }
     }
 
+    private boolean dreamHasFocus() {
+        // Dreams always had focus before they were able to track it.
+        return !dreamTracksFocus() || mController.dreamHasFocus();
+    }
+
     protected void requestStartDreamFromShell() {
         requestDreamInternal();
     }
 
     private void requestDreamInternal() {
+        if (isDreamingInternal() && !dreamHasFocus() && mController.bringDreamToFront()) {
+            return;
+        }
+
         // Ask the power manager to nap.  It will eventually call back into
         // startDream() if/when it is appropriate to start dreaming.
         // Because napping could cause the screen to turn off immediately if the dream
@@ -1128,6 +1140,16 @@
             });
         }
 
+        @Override
+        public void onDreamFocusChanged(boolean hasFocus) {
+            final long ident = Binder.clearCallingIdentity();
+            try {
+                mController.setDreamHasFocus(hasFocus);
+            } finally {
+                Binder.restoreCallingIdentity(ident);
+            }
+        }
+
         boolean canLaunchDreamActivity(String dreamPackageName, String packageName,
                 int callingUid) {
             if (dreamPackageName == null || packageName == null) {
diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubTransactionManager.java b/services/core/java/com/android/server/location/contexthub/ContextHubTransactionManager.java
index b18871c..a0dbfa0 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubTransactionManager.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubTransactionManager.java
@@ -28,6 +28,7 @@
 import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Random;
 import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.ScheduledThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
@@ -77,9 +78,12 @@
     private final AtomicInteger mNextAvailableId = new AtomicInteger();
 
     /**
-     * The next available message sequence number
+     * The next available message sequence number. We choose a random
+     * number to start with to avoid collisions and limit the bound to
+     * half of the max value to avoid overflow.
      */
-    private final AtomicInteger mNextAvailableMessageSequenceNumber = new AtomicInteger();
+    private final AtomicInteger mNextAvailableMessageSequenceNumber =
+            new AtomicInteger(new Random().nextInt(Integer.MAX_VALUE / 2));
 
     /*
      * An executor and the future object for scheduling timeout timers
diff --git a/services/core/java/com/android/server/power/hint/Android.bp b/services/core/java/com/android/server/power/hint/Android.bp
index d7dd902..6dadf8f 100644
--- a/services/core/java/com/android/server/power/hint/Android.bp
+++ b/services/core/java/com/android/server/power/hint/Android.bp
@@ -11,3 +11,10 @@
     name: "power_hint_flags_lib",
     aconfig_declarations: "power_hint_flags",
 }
+
+java_aconfig_library {
+    name: "power_hint_flags_lib_host",
+    aconfig_declarations: "power_hint_flags",
+    host_supported: true,
+    defaults: ["framework-minus-apex-aconfig-java-defaults"],
+}
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index f925b5f..236a746 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -77,6 +77,7 @@
 import static android.content.pm.ActivityInfo.FLAG_SHOW_FOR_ALL_USERS;
 import static android.content.pm.ActivityInfo.FLAG_STATE_NOT_NEEDED;
 import static android.content.pm.ActivityInfo.FLAG_TURN_SCREEN_ON;
+import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_INSETS_DECOUPLED_CONFIGURATION;
 import static android.content.pm.ActivityInfo.INSETS_DECOUPLED_CONFIGURATION_ENFORCED;
 import static android.content.pm.ActivityInfo.LAUNCH_MULTIPLE;
 import static android.content.pm.ActivityInfo.LAUNCH_SINGLE_INSTANCE;
@@ -2119,9 +2120,19 @@
         mCameraCompatControlEnabled = mWmService.mContext.getResources()
                 .getBoolean(R.bool.config_isCameraCompatControlForStretchedIssuesEnabled);
         mResolveConfigHint = new TaskFragment.ConfigOverrideHint();
-        mResolveConfigHint.mUseLegacyInsetsForStableBounds =
-                mWmService.mFlags.mInsetsDecoupledConfiguration
-                        && !info.isChangeEnabled(INSETS_DECOUPLED_CONFIGURATION_ENFORCED);
+        if (mWmService.mFlags.mInsetsDecoupledConfiguration) {
+            // When the stable configuration is the default behavior, override for the legacy apps
+            // without forward override flag.
+            mResolveConfigHint.mUseOverrideInsetsForStableBounds =
+                    !info.isChangeEnabled(INSETS_DECOUPLED_CONFIGURATION_ENFORCED)
+                            && !info.isChangeEnabled(
+                                    OVERRIDE_ENABLE_INSETS_DECOUPLED_CONFIGURATION);
+        } else {
+            // When the stable configuration is not the default behavior, forward overriding the
+            // listed apps.
+            mResolveConfigHint.mUseOverrideInsetsForStableBounds =
+                    info.isChangeEnabled(OVERRIDE_ENABLE_INSETS_DECOUPLED_CONFIGURATION);
+        }
 
         mTargetSdk = info.applicationInfo.targetSdkVersion;
 
@@ -8466,7 +8477,7 @@
         mCompatDisplayInsets =
                 new CompatDisplayInsets(
                         mDisplayContent, this, letterboxedContainerBounds,
-                        mResolveConfigHint.mUseLegacyInsetsForStableBounds);
+                        mResolveConfigHint.mUseOverrideInsetsForStableBounds);
     }
 
     private void clearSizeCompatModeAttributes() {
@@ -8670,7 +8681,7 @@
         if (rotation == ROTATION_UNDEFINED && !isFixedRotationTransforming()) {
             rotation = mDisplayContent.getRotation();
         }
-        if (!mResolveConfigHint.mUseLegacyInsetsForStableBounds
+        if (!mResolveConfigHint.mUseOverrideInsetsForStableBounds
                 || getCompatDisplayInsets() != null
                 || isFloating(parentWindowingMode) || parentAppBounds == null
                 || parentAppBounds.isEmpty() || rotation == ROTATION_UNDEFINED) {
@@ -8987,7 +8998,7 @@
         if (mDisplayContent == null) {
             return true;
         }
-        if (!mResolveConfigHint.mUseLegacyInsetsForStableBounds) {
+        if (!mResolveConfigHint.mUseOverrideInsetsForStableBounds) {
             // No insets should be considered any more.
             return true;
         }
@@ -9006,7 +9017,7 @@
         final Task task = getTask();
         task.calculateInsetFrames(outNonDecorBounds /* outNonDecorBounds */,
                 outStableBounds /* outStableBounds */, parentBounds /* bounds */, di,
-                mResolveConfigHint.mUseLegacyInsetsForStableBounds);
+                mResolveConfigHint.mUseOverrideInsetsForStableBounds);
         final int orientationWithInsets = outStableBounds.height() >= outStableBounds.width()
                 ? ORIENTATION_PORTRAIT : ORIENTATION_LANDSCAPE;
         // If orientation does not match the orientation with insets applied, then a
@@ -9063,7 +9074,7 @@
                 getResolvedOverrideConfiguration().windowConfiguration.getBounds();
         final int stableBoundsOrientation = stableBounds.width() > stableBounds.height()
                 ? ORIENTATION_LANDSCAPE : ORIENTATION_PORTRAIT;
-        final int parentOrientation = mResolveConfigHint.mUseLegacyInsetsForStableBounds
+        final int parentOrientation = mResolveConfigHint.mUseOverrideInsetsForStableBounds
                 ? stableBoundsOrientation : newParentConfig.orientation;
 
         // If the activity requires a different orientation (either by override or activityInfo),
@@ -9088,7 +9099,7 @@
             return;
         }
 
-        final Rect parentAppBounds = mResolveConfigHint.mUseLegacyInsetsForStableBounds
+        final Rect parentAppBounds = mResolveConfigHint.mUseOverrideInsetsForStableBounds
                 ? outNonDecorBounds : newParentConfig.windowConfiguration.getAppBounds();
         // TODO(b/182268157): Explore using only one type of parentBoundsWithInsets, either app
         // bounds or stable bounds to unify aspect ratio logic.
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index a739e57..b3c43bc 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -3736,6 +3736,14 @@
             return false;
         }
 
+        // If the app is using auto-enter, and it explicitly requests entering PiP while pausing,
+        // return false immediately since auto-enter should take in place instead.
+        if (fromClient && r.isState(PAUSING) && params.isAutoEnterEnabled()) {
+            Slog.w(TAG, "Skip client enterPictureInPictureMode request while pausing,"
+                    + " auto-enter-pip is enabled");
+            return false;
+        }
+
         if (isPip2ExperimentEnabled()) {
             // If PiP2 flag is on and request to enter PiP comes in,
             // we request a direct transition TRANSIT_PIP from Shell to get the right entry bounds.
diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java
index cb5ad91..da1581e 100644
--- a/services/core/java/com/android/server/wm/LetterboxUiController.java
+++ b/services/core/java/com/android/server/wm/LetterboxUiController.java
@@ -905,13 +905,11 @@
     // Note that we check the task rather than the parent as with ActivityEmbedding the parent might
     // be a TaskFragment, and its windowing mode is always MULTI_WINDOW, even if the task is
     // actually fullscreen.
-    private boolean isDisplayFullScreenAndInPosture(DeviceStateController.DeviceState state,
-            boolean isTabletop) {
+    private boolean isDisplayFullScreenAndInPosture(boolean isTabletop) {
         Task task = mActivityRecord.getTask();
-        return mActivityRecord.mDisplayContent != null
-                && mActivityRecord.mDisplayContent.getDisplayRotation().isDeviceInPosture(state,
-                    isTabletop)
-                && task != null
+        return mActivityRecord.mDisplayContent != null && task != null
+                && mActivityRecord.mDisplayContent.getDisplayRotation().isDeviceInPosture(
+                        DeviceStateController.DeviceState.HALF_FOLDED, isTabletop)
                 && task.getWindowingMode() == WINDOWING_MODE_FULLSCREEN;
     }
 
@@ -939,16 +937,14 @@
     }
 
     private boolean isFullScreenAndBookModeEnabled() {
-        return isDisplayFullScreenAndInPosture(
-                DeviceStateController.DeviceState.HALF_FOLDED, false /* isTabletop */)
+        return isDisplayFullScreenAndInPosture(/* isTabletop */ false)
                 && mLetterboxConfiguration.getIsAutomaticReachabilityInBookModeEnabled();
     }
 
     float getVerticalPositionMultiplier(Configuration parentConfiguration) {
         // Don't check resolved configuration because it may not be updated yet during
         // configuration change.
-        boolean tabletopMode = isDisplayFullScreenAndInPosture(
-                DeviceStateController.DeviceState.HALF_FOLDED, true /* isTabletop */);
+        boolean tabletopMode = isDisplayFullScreenAndInPosture(/* isTabletop */ true);
         return isVerticalReachabilityEnabled(parentConfiguration)
                 // Using the last global dynamic position to avoid "jumps" when moving
                 // between apps or activities.
@@ -981,16 +977,15 @@
     }
 
     private boolean shouldUseSplitScreenAspectRatio(@NonNull Configuration parentConfiguration) {
-        final boolean isBookMode = isDisplayFullScreenAndInPosture(
-                DeviceStateController.DeviceState.HALF_FOLDED,
-                /* isTabletop */ false);
+        final boolean isBookMode = isDisplayFullScreenAndInPosture(/* isTabletop */ false);
         final boolean isNotCenteredHorizontally = getHorizontalPositionMultiplier(
                 parentConfiguration) != LETTERBOX_POSITION_MULTIPLIER_CENTER;
-        final boolean isTabletopMode = isDisplayFullScreenAndInPosture(
-                DeviceStateController.DeviceState.HALF_FOLDED,
-                /* isTabletop */ true);
+        final boolean isTabletopMode = isDisplayFullScreenAndInPosture(/* isTabletop */ true);
+        final boolean isLandscape = isFixedOrientationLandscape(
+                mActivityRecord.getOverrideOrientation());
+
         // Don't resize to split screen size when in book mode if letterbox position is centered
-        return ((isBookMode && isNotCenteredHorizontally) || isTabletopMode)
+        return (isBookMode && isNotCenteredHorizontally || isTabletopMode && isLandscape)
                     || isCameraCompatSplitScreenAspectRatioAllowed()
                         && isCameraCompatTreatmentActive();
     }
@@ -1647,17 +1642,13 @@
         if (isHorizontalReachabilityEnabled()) {
             int letterboxPositionForHorizontalReachability = getLetterboxConfiguration()
                     .getLetterboxPositionForHorizontalReachability(
-                            isDisplayFullScreenAndInPosture(
-                                    DeviceStateController.DeviceState.HALF_FOLDED,
-                                    false /* isTabletop */));
+                            isDisplayFullScreenAndInPosture(/* isTabletop */ false));
             positionToLog = letterboxHorizontalReachabilityPositionToLetterboxPosition(
                     letterboxPositionForHorizontalReachability);
         } else if (isVerticalReachabilityEnabled()) {
             int letterboxPositionForVerticalReachability = getLetterboxConfiguration()
                     .getLetterboxPositionForVerticalReachability(
-                            isDisplayFullScreenAndInPosture(
-                                    DeviceStateController.DeviceState.HALF_FOLDED,
-                                    true /* isTabletop */));
+                            isDisplayFullScreenAndInPosture(/* isTabletop */ true));
             positionToLog = letterboxVerticalReachabilityPositionToLetterboxPosition(
                     letterboxPositionForVerticalReachability);
         }
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index 7a8bea0..2b631f7 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -2204,7 +2204,7 @@
     static class ConfigOverrideHint {
         @Nullable DisplayInfo mTmpOverrideDisplayInfo;
         @Nullable ActivityRecord.CompatDisplayInsets mTmpCompatInsets;
-        boolean mUseLegacyInsetsForStableBounds;
+        boolean mUseOverrideInsetsForStableBounds;
     }
 
     void computeConfigResourceOverrides(@NonNull Configuration inOutConfig,
@@ -2237,11 +2237,11 @@
             @NonNull Configuration parentConfig, @Nullable ConfigOverrideHint overrideHint) {
         DisplayInfo overrideDisplayInfo = null;
         ActivityRecord.CompatDisplayInsets compatInsets = null;
-        boolean useLegacyInsetsForStableBounds = false;
+        boolean useOverrideInsetsForStableBounds = false;
         if (overrideHint != null) {
             overrideDisplayInfo = overrideHint.mTmpOverrideDisplayInfo;
             compatInsets = overrideHint.mTmpCompatInsets;
-            useLegacyInsetsForStableBounds = overrideHint.mUseLegacyInsetsForStableBounds;
+            useOverrideInsetsForStableBounds = overrideHint.mUseOverrideInsetsForStableBounds;
             if (overrideDisplayInfo != null) {
                 // Make sure the screen related configs can be computed by the provided
                 // display info.
@@ -2321,7 +2321,7 @@
                 // The non decor inset are areas that could never be removed in Honeycomb. See
                 // {@link WindowManagerPolicy#getNonDecorInsetsLw}.
                 calculateInsetFrames(mTmpNonDecorBounds, mTmpStableBounds, mTmpFullBounds, di,
-                        useLegacyInsetsForStableBounds);
+                        useOverrideInsetsForStableBounds);
             } else {
                 // Apply the given non-decor and stable insets to calculate the corresponding bounds
                 // for screen size of configuration.
diff --git a/services/core/java/com/android/server/wm/WindowManagerInternal.java b/services/core/java/com/android/server/wm/WindowManagerInternal.java
index 407b5d5..eeedec3 100644
--- a/services/core/java/com/android/server/wm/WindowManagerInternal.java
+++ b/services/core/java/com/android/server/wm/WindowManagerInternal.java
@@ -1044,6 +1044,13 @@
             int[] fromOrientations, int[] toOrientations);
 
     /**
+     * Set current screen capture session id that will be used during sensitive content protections.
+     *
+     * @param sessionId Session id for this screen capture protection
+     */
+    public abstract void setBlockScreenCaptureForAppsSessionId(long sessionId);
+
+    /**
      * Set whether screen capture should be disabled for all windows of a specific app windows based
      * on sensitive content protections.
      *
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 07e534c..de7c0eb 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -44,6 +44,7 @@
 import static android.os.Process.myUid;
 import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
 import static android.permission.flags.Flags.sensitiveContentImprovements;
+import static android.permission.flags.Flags.sensitiveContentMetricsBugfix;
 import static android.provider.Settings.Global.DEVELOPMENT_ENABLE_FREEFORM_WINDOWS_SUPPORT;
 import static android.provider.Settings.Global.DEVELOPMENT_ENABLE_NON_RESIZABLE_MULTI_WINDOW;
 import static android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS;
@@ -113,6 +114,7 @@
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_WINDOW_MOVEMENT;
 import static com.android.internal.protolog.ProtoLogGroup.WM_ERROR;
 import static com.android.internal.protolog.ProtoLogGroup.WM_SHOW_TRANSACTIONS;
+import static com.android.internal.util.FrameworkStatsLog.SENSITIVE_NOTIFICATION_APP_PROTECTION_APPLIED;
 import static com.android.internal.util.LatencyTracker.ACTION_ROTATE_SCREEN;
 import static com.android.server.LockGuard.INDEX_WINDOW;
 import static com.android.server.LockGuard.installLock;
@@ -340,6 +342,7 @@
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.internal.util.DumpUtils;
 import com.android.internal.util.FastPrintWriter;
+import com.android.internal.util.FrameworkStatsLog;
 import com.android.internal.util.LatencyTracker;
 import com.android.internal.view.WindowManagerPolicyThread;
 import com.android.server.AnimationThread;
@@ -1110,6 +1113,9 @@
     SystemPerformanceHinter mSystemPerformanceHinter;
 
     @GuardedBy("mGlobalLock")
+    private long mSensitiveContentProtectionSessionId = 0;
+
+    @GuardedBy("mGlobalLock")
     final SensitiveContentPackages mSensitiveContentPackages = new SensitiveContentPackages();
     /**
      * UIDs for which a Toast has been shown to indicate
@@ -4519,7 +4525,8 @@
         }
     }
 
-    int getDisplayUserRotation(int displayId) {
+    @Override
+    public int getDisplayUserRotation(int displayId) {
         synchronized (mGlobalLock) {
             final DisplayContent display = mRoot.getDisplayContent(displayId);
             if (display == null) {
@@ -8739,6 +8746,16 @@
         }
 
         @Override
+        public void setBlockScreenCaptureForAppsSessionId(long sessionId) {
+            synchronized (mGlobalLock) {
+                if (sensitiveContentMetricsBugfix()
+                        && mSensitiveContentProtectionSessionId != sessionId) {
+                    mSensitiveContentProtectionSessionId = sessionId;
+                }
+            }
+        }
+
+        @Override
         public void addBlockScreenCaptureForApps(ArraySet<PackageInfo> packageInfos) {
             synchronized (mGlobalLock) {
                 boolean modified =
@@ -10202,6 +10219,15 @@
                                 Toast.LENGTH_SHORT)
                         .show();
             });
+            // If blocked due to notification protection (null window token) log protection applied
+            if (sensitiveContentMetricsBugfix()
+                    && mSensitiveContentPackages
+                    .shouldBlockScreenCaptureForApp(w.getOwningPackage(), uid, null)) {
+                FrameworkStatsLog.write(
+                        SENSITIVE_NOTIFICATION_APP_PROTECTION_APPLIED,
+                        mSensitiveContentProtectionSessionId,
+                        uid);
+            }
         }
     }
 }
diff --git a/services/core/java/com/android/server/wm/WindowProcessController.java b/services/core/java/com/android/server/wm/WindowProcessController.java
index 9d8246d..d91a211 100644
--- a/services/core/java/com/android/server/wm/WindowProcessController.java
+++ b/services/core/java/com/android/server/wm/WindowProcessController.java
@@ -631,6 +631,11 @@
             return;
         }
 
+        final WindowProcessController caller = mAtm.mProcessMap.getProcess(r.launchedFromPid);
+        if (caller != null && caller.mInstrumenting) {
+            return;
+        }
+
         final long diff = launchTime - lastLaunchTime;
         if (diff < RAPID_ACTIVITY_LAUNCH_MS) {
             mRapidActivityLaunchCount++;
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyCacheImpl.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyCacheImpl.java
index c4e2dc8..b76279b 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyCacheImpl.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyCacheImpl.java
@@ -32,7 +32,6 @@
 import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
-import java.util.concurrent.atomic.AtomicBoolean;
 
 /**
  * Implementation of {@link DevicePolicyCache}, to which {@link DevicePolicyManagerService} pushes
@@ -48,18 +47,11 @@
     private final Object mLock = new Object();
 
     /**
-     * Indicates which user is screen capture disallowed on. Can be {@link UserHandle#USER_NULL},
-     * {@link UserHandle#USER_ALL} or a concrete user ID.
-     */
-    @GuardedBy("mLock")
-    private int mScreenCaptureDisallowedUser = UserHandle.USER_NULL;
-
-    /**
      * Indicates if screen capture is disallowed on a specific user or all users if
      * it contains {@link UserHandle#USER_ALL}.
      */
     @GuardedBy("mLock")
-    private Set<Integer> mScreenCaptureDisallowedUsers = new HashSet<>();
+    private final Set<Integer> mScreenCaptureDisallowedUsers = new HashSet<>();
 
     @GuardedBy("mLock")
     private final SparseIntArray mPasswordQuality = new SparseIntArray();
@@ -70,9 +62,8 @@
     @GuardedBy("mLock")
     private ArrayMap<String, String> mLauncherShortcutOverrides = new ArrayMap<>();
 
-
     /** Maps to {@code ActiveAdmin.mAdminCanGrantSensorsPermissions}. */
-    private final AtomicBoolean mCanGrantSensorsPermissions = new AtomicBoolean(false);
+    private volatile boolean mCanGrantSensorsPermissions = false;
 
     @GuardedBy("mLock")
     private final SparseIntArray mContentProtectionPolicy = new SparseIntArray();
@@ -87,10 +78,6 @@
 
     @Override
     public boolean isScreenCaptureAllowed(int userHandle) {
-        return isScreenCaptureAllowedInPolicyEngine(userHandle);
-    }
-
-    private boolean isScreenCaptureAllowedInPolicyEngine(int userHandle) {
         // This won't work if resolution mechanism is not strictest applies, but it's ok for now.
         synchronized (mLock) {
             return !mScreenCaptureDisallowedUsers.contains(userHandle)
@@ -98,18 +85,6 @@
         }
     }
 
-    public int getScreenCaptureDisallowedUser() {
-        synchronized (mLock) {
-            return mScreenCaptureDisallowedUser;
-        }
-    }
-
-    public void setScreenCaptureDisallowedUser(int userHandle) {
-        synchronized (mLock) {
-            mScreenCaptureDisallowedUser = userHandle;
-        }
-    }
-
     public void setScreenCaptureDisallowedUser(int userHandle, boolean disallowed) {
         synchronized (mLock) {
             if (disallowed) {
@@ -170,12 +145,12 @@
 
     @Override
     public boolean canAdminGrantSensorsPermissions() {
-        return mCanGrantSensorsPermissions.get();
+        return mCanGrantSensorsPermissions;
     }
 
     /** Sets admin control over permission grants. */
     public void setAdminCanGrantSensorsPermissions(boolean canGrant) {
-        mCanGrantSensorsPermissions.set(canGrant);
+        mCanGrantSensorsPermissions = canGrant;
     }
 
     @Override
@@ -205,7 +180,7 @@
             pw.println("Password quality: " + mPasswordQuality);
             pw.println("Permission policy: " + mPermissionPolicy);
             pw.println("Content protection policy: " + mContentProtectionPolicy);
-            pw.println("Admin can grant sensors permission: " + mCanGrantSensorsPermissions.get());
+            pw.println("Admin can grant sensors permission: " + mCanGrantSensorsPermissions);
             pw.print("Shortcuts overrides: ");
             pw.println(mLauncherShortcutOverrides);
             pw.decreaseIndent();
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 7761311..6458eac 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -12320,7 +12320,8 @@
         if (Flags.headlessDeviceOwnerSingleUserEnabled()) {
             // Block this method if the device is in headless main user mode
             Preconditions.checkCallAuthorization(
-                    getHeadlessDeviceOwnerModeForDeviceOwner()
+                    !mInjector.userManagerIsHeadlessSystemUserMode()
+                            || getHeadlessDeviceOwnerModeForDeviceOwner()
                             != HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER,
                     "createAndManageUser was called while in headless single user mode");
         }
@@ -23319,10 +23320,12 @@
     }
 
     private boolean hasAdminPolicy(int adminPolicy, String callerPackageName) {
-        CallerIdentity caller = getCallerIdentity(callerPackageName);
-        ActiveAdmin deviceAdmin = getActiveAdminWithPolicyForUidLocked(
-                null, adminPolicy, caller.getUid());
-        return deviceAdmin != null;
+        synchronized (getLockObject()) {
+            CallerIdentity caller = getCallerIdentity(callerPackageName);
+            ActiveAdmin deviceAdmin = getActiveAdminWithPolicyForUidLocked(
+                    null, adminPolicy, caller.getUid());
+            return deviceAdmin != null;
+        }
     }
 
     /**
diff --git a/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleStateTransitions.java b/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleStateTransitions.java
index 16daacb..aa7532a 100644
--- a/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleStateTransitions.java
+++ b/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleStateTransitions.java
@@ -412,7 +412,7 @@
                 /* stickyKeepInnerUntil45Degrees */ true,
                 PreferredScreen.INNER,
                 /* setStickyKeepOuterUntil90Degrees */ null,
-                /* setStickyKeepInnerUntil45Degrees */ false
+                /* setStickyKeepInnerUntil45Degrees */ null
         ));
         DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
                 HingeAngle.ANGLE_45_TO_90,
@@ -492,7 +492,7 @@
                 /* stickyKeepInnerUntil45Degrees */ true,
                 PreferredScreen.INNER,
                 /* setStickyKeepOuterUntil90Degrees */ null,
-                /* setStickyKeepInnerUntil45Degrees */ false
+                /* setStickyKeepInnerUntil45Degrees */ null
         ));
         DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
                 HingeAngle.ANGLE_45_TO_90,
diff --git a/services/foldables/devicestateprovider/tests/src/com/android/server/policy/BookStyleDeviceStatePolicyTest.java b/services/foldables/devicestateprovider/tests/src/com/android/server/policy/BookStyleDeviceStatePolicyTest.java
index 9f07aa8..2d725d1 100644
--- a/services/foldables/devicestateprovider/tests/src/com/android/server/policy/BookStyleDeviceStatePolicyTest.java
+++ b/services/foldables/devicestateprovider/tests/src/com/android/server/policy/BookStyleDeviceStatePolicyTest.java
@@ -511,7 +511,7 @@
     }
 
     @Test
-    public void test_unfoldTo60Degrees_andFoldTo10_switchesToClosedState() {
+    public void test_unfoldTo60Degrees_andFoldTo10_doesNotSwitchToClosedState() {
         sendHingeAngle(0f);
         sendRightSideFlatSensorEvent(false);
         mProvider.setListener(mListener);
@@ -522,6 +522,36 @@
 
         sendHingeAngle(10f);
 
+        verify(mListener, never()).onStateChanged(anyInt());
+    }
+
+    @Test
+    public void test_unfoldTo100Degrees_andFoldTo10_switchesToClosedState() {
+        sendHingeAngle(0f);
+        sendRightSideFlatSensorEvent(false);
+        mProvider.setListener(mListener);
+        assertLatestReportedState(DEVICE_STATE_CLOSED);
+        sendHingeAngle(100f);
+        assertLatestReportedState(DEVICE_STATE_HALF_OPENED);
+        clearInvocations(mListener);
+
+        sendHingeAngle(10f);
+
+        verify(mListener).onStateChanged(DEVICE_STATE_CLOSED);
+    }
+
+    @Test
+    public void test_unfoldTo10Degrees_andFoldTo0_switchesToClosedState() {
+        sendHingeAngle(0f);
+        sendRightSideFlatSensorEvent(false);
+        mProvider.setListener(mListener);
+        assertLatestReportedState(DEVICE_STATE_CLOSED);
+        sendHingeAngle(10f);
+        assertLatestReportedState(DEVICE_STATE_HALF_OPENED);
+        clearInvocations(mListener);
+
+        sendHingeAngle(0f);
+
         verify(mListener).onStateChanged(DEVICE_STATE_CLOSED);
     }
 
diff --git a/services/permission/java/com/android/server/permission/access/appop/AppOpService.kt b/services/permission/java/com/android/server/permission/access/appop/AppOpService.kt
index c0d988d..b0c7073 100644
--- a/services/permission/java/com/android/server/permission/access/appop/AppOpService.kt
+++ b/services/permission/java/com/android/server/permission/access/appop/AppOpService.kt
@@ -20,6 +20,7 @@
 import android.companion.virtual.VirtualDeviceManager
 import android.os.Handler
 import android.os.UserHandle
+import android.permission.PermissionManager
 import android.permission.flags.Flags
 import android.util.ArrayMap
 import android.util.ArraySet
@@ -142,7 +143,7 @@
         }
     }
 
-    override fun getNonDefaultUidModes(uid: Int, persistentDeviceId: String): SparseIntArray {
+    override fun getNonDefaultUidModes(uid: Int, deviceId: String): SparseIntArray {
         val appId = UserHandle.getAppId(uid)
         val userId = UserHandle.getUserId(uid)
         service.getState {
@@ -150,7 +151,8 @@
                 with(appIdPolicy) { opNameMapToOpSparseArray(getAppOpModes(appId, userId)?.map) }
             if (Flags.runtimePermissionAppopsMappingEnabled()) {
                 runtimePermissionNameToAppOp.forEachIndexed { _, permissionName, appOpCode ->
-                    val mode = getUidModeFromPermissionState(appId, userId, permissionName)
+                    val mode =
+                        getUidModeFromPermissionState(appId, userId, permissionName, deviceId)
                     if (mode != AppOpsManager.opToDefaultMode(appOpCode)) {
                         modes[appOpCode] = mode
                     }
@@ -165,7 +167,7 @@
         return opNameMapToOpSparseArray(getPackageModes(packageName, userId))
     }
 
-    override fun getUidMode(uid: Int, persistentDeviceId: String, op: Int): Int {
+    override fun getUidMode(uid: Int, deviceId: String, op: Int): Int {
         val appId = UserHandle.getAppId(uid)
         val userId = UserHandle.getUserId(uid)
         val opName = AppOpsManager.opToPublicName(op)
@@ -174,7 +176,9 @@
         return if (!Flags.runtimePermissionAppopsMappingEnabled() || permissionName == null) {
             service.getState { with(appIdPolicy) { getAppOpMode(appId, userId, opName) } }
         } else {
-            service.getState { getUidModeFromPermissionState(appId, userId, permissionName) }
+            service.getState {
+                getUidModeFromPermissionState(appId, userId, permissionName, deviceId)
+            }
         }
     }
 
@@ -187,15 +191,31 @@
     private fun GetStateScope.getUidModeFromPermissionState(
         appId: Int,
         userId: Int,
-        permissionName: String
+        permissionName: String,
+        deviceId: String
     ): Int {
+        val checkDevicePermissionFlags =
+            deviceId != VirtualDeviceManager.PERSISTENT_DEVICE_ID_DEFAULT &&
+                permissionName in PermissionManager.DEVICE_AWARE_PERMISSIONS
         val permissionFlags =
-            with(permissionPolicy) { getPermissionFlags(appId, userId, permissionName) }
+            if (checkDevicePermissionFlags) {
+                with(devicePermissionPolicy) {
+                    getPermissionFlags(appId, deviceId, userId, permissionName)
+                }
+            } else {
+                with(permissionPolicy) { getPermissionFlags(appId, userId, permissionName) }
+            }
         val backgroundPermissionName = foregroundToBackgroundPermissionName[permissionName]
         val backgroundPermissionFlags =
             if (backgroundPermissionName != null) {
-                with(permissionPolicy) {
-                    getPermissionFlags(appId, userId, backgroundPermissionName)
+                if (checkDevicePermissionFlags) {
+                    with(devicePermissionPolicy) {
+                        getPermissionFlags(appId, deviceId, userId, backgroundPermissionName)
+                    }
+                } else {
+                    with(permissionPolicy) {
+                        getPermissionFlags(appId, userId, backgroundPermissionName)
+                    }
                 }
             } else {
                 PermissionFlags.RUNTIME_GRANTED
@@ -207,7 +227,7 @@
 
         val fullerPermissionName =
             PermissionService.getFullerPermission(permissionName) ?: return result
-        return getUidModeFromPermissionState(appId, userId, fullerPermissionName)
+        return getUidModeFromPermissionState(appId, userId, fullerPermissionName, deviceId)
     }
 
     private fun evaluateModeFromPermissionFlags(
@@ -224,7 +244,7 @@
             MODE_IGNORED
         }
 
-    override fun setUidMode(uid: Int, persistentDeviceId: String, code: Int, mode: Int): Boolean {
+    override fun setUidMode(uid: Int, deviceId: String, code: Int, mode: Int): Boolean {
         if (
             Flags.runtimePermissionAppopsMappingEnabled() && code in runtimeAppOpToPermissionNames
         ) {
@@ -308,7 +328,7 @@
         // and we have our own persistence.
     }
 
-    override fun getForegroundOps(uid: Int, persistentDeviceId: String): SparseBooleanArray {
+    override fun getForegroundOps(uid: Int, deviceId: String): SparseBooleanArray {
         return SparseBooleanArray().apply {
             getUidModes(uid)?.forEachIndexed { _, op, mode ->
                 if (mode == AppOpsManager.MODE_FOREGROUND) {
@@ -317,7 +337,7 @@
             }
             if (Flags.runtimePermissionAppopsMappingEnabled()) {
                 foregroundableOps.forEachIndexed { _, op, _ ->
-                    if (getUidMode(uid, persistentDeviceId, op) == AppOpsManager.MODE_FOREGROUND) {
+                    if (getUidMode(uid, deviceId, op) == AppOpsManager.MODE_FOREGROUND) {
                         this[op] = true
                     }
                 }
@@ -501,7 +521,7 @@
                         )
                     }
                 }
-                ?: runtimePermissionNameToAppOp[permissionName]?.let { appOpCode ->
+                    ?: runtimePermissionNameToAppOp[permissionName]?.let { appOpCode ->
                     addPendingChangedModeIfNeeded(
                         appId,
                         userId,
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
index de93914..78ebdb1 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
@@ -1312,7 +1312,7 @@
         when(mHolder.hbmController.getHighBrightnessMode()).thenReturn(
                 BrightnessInfo.HIGH_BRIGHTNESS_MODE_HDR);
         when(mHolder.hbmController.getHdrBrightnessValue()).thenReturn(PowerManager.BRIGHTNESS_MAX);
-        when(mHolder.hdrClamper.getMaxBrightness()).thenReturn(clampedBrightness);
+        when(mHolder.hdrClamper.clamp(PowerManager.BRIGHTNESS_MAX)).thenReturn(clampedBrightness);
         when(mHolder.hdrClamper.getTransitionRate()).thenReturn(transitionRate);
 
         mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/HdrClamperTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/HdrClamperTest.java
index 39ffe5b..c785ea6 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/HdrClamperTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/HdrClamperTest.java
@@ -221,6 +221,18 @@
         assertEquals(0.04f, mHdrClamper.getTransitionRate(), FLOAT_TOLERANCE);
     }
 
+    @Test
+    public void testCalmper_transitionRateOverriddenByOtherRequest() {
+        mHdrClamper.onAmbientLuxChange(499);
+
+        mClock.fastForward(3000);
+        mTestHandler.timeAdvance();
+        assertEquals(0.6f, mHdrClamper.getMaxBrightness(), FLOAT_TOLERANCE);
+        assertEquals(0.04, mHdrClamper.getTransitionRate(), FLOAT_TOLERANCE);
+        // getTransitionRate should reset transitionRate
+        assertEquals(-1f, mHdrClamper.getTransitionRate(), FLOAT_TOLERANCE);
+    }
+
     // MsgInfo.sendTime is calculated first by adding SystemClock.uptimeMillis()
     // (in Handler.sendMessageDelayed) and then by subtracting SystemClock.uptimeMillis()
     // (in TestHandler.sendMessageAtTime, there might be several milliseconds difference between
diff --git a/services/tests/dreamservicetests/src/com/android/server/dreams/DreamAccessibilityTest.java b/services/tests/dreamservicetests/src/com/android/server/dreams/DreamAccessibilityTest.java
new file mode 100644
index 0000000..99968d5
--- /dev/null
+++ b/services/tests/dreamservicetests/src/com/android/server/dreams/DreamAccessibilityTest.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.dreams;
+
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.service.dreams.utils.DreamAccessibility;
+import android.text.TextUtils;
+import android.view.View;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.R;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.Collections;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class DreamAccessibilityTest {
+
+    @Mock
+    private View mView;
+
+    @Mock
+    private Context mContext;
+
+    @Mock
+    private Resources mResources;
+
+    @Mock
+    private AccessibilityNodeInfo mAccessibilityNodeInfo;
+
+    @Captor
+    private ArgumentCaptor<View.AccessibilityDelegate> mAccessibilityDelegateArgumentCaptor;
+
+    private DreamAccessibility mDreamAccessibility;
+    private static final String CUSTOM_ACTION = "Custom Action";
+    private static final String EXISTING_ACTION = "Existing Action";
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mDreamAccessibility = new DreamAccessibility(mContext, mView);
+
+        when(mContext.getResources()).thenReturn(mResources);
+        when(mResources.getString(R.string.dream_accessibility_action_click))
+                .thenReturn(CUSTOM_ACTION);
+    }
+    /**
+     * Test to verify the configuration of accessibility actions within a view delegate.
+     */
+    @Test
+    public void testConfigureAccessibilityActions() {
+        when(mAccessibilityNodeInfo.getActionList()).thenReturn(new ArrayList<>());
+
+        mDreamAccessibility.updateAccessibilityConfiguration(false);
+
+        verify(mView).setAccessibilityDelegate(mAccessibilityDelegateArgumentCaptor.capture());
+        View.AccessibilityDelegate capturedDelegate =
+                mAccessibilityDelegateArgumentCaptor.getValue();
+
+        capturedDelegate.onInitializeAccessibilityNodeInfo(mView, mAccessibilityNodeInfo);
+
+        verify(mAccessibilityNodeInfo).addAction(argThat(action ->
+                action.getId() == AccessibilityNodeInfo.ACTION_CLICK
+                        && TextUtils.equals(action.getLabel(), CUSTOM_ACTION)));
+    }
+
+    /**
+     * Test to verify the configuration of accessibility actions within a view delegate,
+     * specifically checking the removal of an existing click action and addition
+     * of a new custom action.
+     */
+    @Test
+    public void testConfigureAccessibilityActions_RemovesExistingClickAction() {
+        AccessibilityNodeInfo.AccessibilityAction existingAction =
+                new AccessibilityNodeInfo.AccessibilityAction(AccessibilityNodeInfo.ACTION_CLICK,
+                        EXISTING_ACTION);
+        when(mAccessibilityNodeInfo.getActionList())
+                .thenReturn(Collections.singletonList(existingAction));
+
+        mDreamAccessibility.updateAccessibilityConfiguration(false);
+
+        verify(mView).setAccessibilityDelegate(mAccessibilityDelegateArgumentCaptor.capture());
+        View.AccessibilityDelegate capturedDelegate =
+                mAccessibilityDelegateArgumentCaptor.getValue();
+
+        capturedDelegate.onInitializeAccessibilityNodeInfo(mView, mAccessibilityNodeInfo);
+
+        verify(mAccessibilityNodeInfo).removeAction(existingAction);
+        verify(mAccessibilityNodeInfo).addAction(argThat(action ->
+                action.getId() == AccessibilityNodeInfo.ACTION_CLICK
+                        && TextUtils.equals(action.getLabel(), CUSTOM_ACTION)));
+
+    }
+
+    /**
+     * Test to verify the removal of a custom accessibility action within a view delegate.
+     */
+    @Test
+    public void testRemoveCustomAccessibilityAction() {
+
+        AccessibilityNodeInfo.AccessibilityAction existingAction =
+                new AccessibilityNodeInfo.AccessibilityAction(AccessibilityNodeInfo.ACTION_CLICK,
+                        EXISTING_ACTION);
+        when(mAccessibilityNodeInfo.getActionList())
+                .thenReturn(Collections.singletonList(existingAction));
+
+        mDreamAccessibility.updateAccessibilityConfiguration(false);
+        verify(mView).setAccessibilityDelegate(mAccessibilityDelegateArgumentCaptor.capture());
+        View.AccessibilityDelegate capturedDelegate =
+                mAccessibilityDelegateArgumentCaptor.getValue();
+        when(mView.getAccessibilityDelegate()).thenReturn(capturedDelegate);
+        clearInvocations(mView);
+
+        mDreamAccessibility.updateAccessibilityConfiguration(true);
+        verify(mView).setAccessibilityDelegate(null);
+    }
+
+    /**
+     * Test to verify the removal of custom accessibility action is not called if delegate is not
+     * set by the dreamService.
+     */
+    @Test
+    public void testRemoveCustomAccessibility_DoesNotRemoveDelegateNotSetByDreamAccessibility() {
+        mDreamAccessibility.updateAccessibilityConfiguration(true);
+        verify(mView, never()).setAccessibilityDelegate(any());
+    }
+}
+
diff --git a/services/tests/dreamservicetests/src/com/android/server/dreams/DreamControllerTest.java b/services/tests/dreamservicetests/src/com/android/server/dreams/DreamControllerTest.java
index db70434..88ab871 100644
--- a/services/tests/dreamservicetests/src/com/android/server/dreams/DreamControllerTest.java
+++ b/services/tests/dreamservicetests/src/com/android/server/dreams/DreamControllerTest.java
@@ -19,6 +19,8 @@
 import static android.os.PowerManager.USER_ACTIVITY_EVENT_OTHER;
 import static android.os.PowerManager.USER_ACTIVITY_FLAG_NO_CHANGE_LIGHTS;
 
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.anyString;
@@ -270,6 +272,31 @@
                 eq(USER_ACTIVITY_FLAG_NO_CHANGE_LIGHTS));
     }
 
+    @Test
+    public void setDreamHasFocus_true_dreamHasFocus() {
+        mDreamController.startDream(mToken, mDreamName, false /*isPreview*/, false /*doze*/,
+                0 /*userId*/, null /*wakeLock*/, mOverlayName, "test" /*reason*/);
+
+        mDreamController.setDreamHasFocus(true);
+        assertTrue(mDreamController.dreamHasFocus());
+    }
+
+    @Test
+    public void setDreamHasFocus_false_dreamDoesNotHaveFocus() {
+        mDreamController.startDream(mToken, mDreamName, false /*isPreview*/, false /*doze*/,
+                0 /*userId*/, null /*wakeLock*/, mOverlayName, "test" /*reason*/);
+
+        mDreamController.setDreamHasFocus(false);
+        assertFalse(mDreamController.dreamHasFocus());
+    }
+
+    @Test
+    public void setDreamHasFocus_notDreaming_dreamDoesNotHaveFocus() {
+        mDreamController.setDreamHasFocus(true);
+        // Dream still doesn't have focus because it was never started.
+        assertFalse(mDreamController.dreamHasFocus());
+    }
+
     private ServiceConnection captureServiceConnection() {
         verify(mContext).bindServiceAsUser(any(), mServiceConnectionACaptor.capture(), anyInt(),
                 any());
diff --git a/services/tests/mockingservicestests/src/com/android/server/SensitiveContentProtectionManagerServiceNotificationTest.java b/services/tests/mockingservicestests/src/com/android/server/SensitiveContentProtectionManagerServiceNotificationTest.java
index 124ae20..a20d935 100644
--- a/services/tests/mockingservicestests/src/com/android/server/SensitiveContentProtectionManagerServiceNotificationTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/SensitiveContentProtectionManagerServiceNotificationTest.java
@@ -16,15 +16,18 @@
 
 package com.android.server;
 
+import static android.permission.flags.Flags.FLAG_SENSITIVE_CONTENT_METRICS_BUGFIX;
 import static android.permission.flags.Flags.FLAG_SENSITIVE_NOTIFICATION_APP_PROTECTION;
 
 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
 
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doCallRealMethod;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyZeroInteractions;
@@ -34,6 +37,7 @@
 import android.media.projection.MediaProjectionInfo;
 import android.media.projection.MediaProjectionManager;
 import android.os.Process;
+import android.platform.test.annotations.RequiresFlagsDisabled;
 import android.platform.test.annotations.RequiresFlagsEnabled;
 import android.platform.test.flag.junit.CheckFlagsRule;
 import android.platform.test.flag.junit.DeviceFlagsValueProvider;
@@ -309,6 +313,26 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(FLAG_SENSITIVE_CONTENT_METRICS_BUGFIX)
+    public void mediaProjectionOnStart_flagDisabled_neverSetBlockScreenCaptureForAppsSessionId() {
+        setupSensitiveNotification();
+
+        mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo());
+
+        verify(mWindowManager, never()).setBlockScreenCaptureForAppsSessionId(anyLong());
+    }
+
+    @Test
+    @RequiresFlagsEnabled(FLAG_SENSITIVE_CONTENT_METRICS_BUGFIX)
+    public void mediaProjectionOnStart_setBlockScreenCaptureForAppsSessionId() {
+        setupSensitiveNotification();
+
+        mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo());
+
+        verify(mWindowManager).setBlockScreenCaptureForAppsSessionId(anyLong());
+    }
+
+    @Test
     public void mediaProjectionOnStart_onProjectionStart_setWmBlockedPackages() {
         ArraySet<PackageInfo> expectedBlockedPackages = setupSensitiveNotification();
 
@@ -323,7 +347,7 @@
 
         mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo());
 
-        verifyZeroInteractions(mWindowManager);
+        verify(mWindowManager, never()).addBlockScreenCaptureForApps(any());
     }
 
     @Test
@@ -332,7 +356,7 @@
 
         mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo());
 
-        verifyZeroInteractions(mWindowManager);
+        verify(mWindowManager, never()).addBlockScreenCaptureForApps(any());
     }
 
     @Test
@@ -400,7 +424,7 @@
 
         mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo());
 
-        verifyZeroInteractions(mWindowManager);
+        verify(mWindowManager, never()).addBlockScreenCaptureForApps(any());
     }
 
     @Test
@@ -411,7 +435,7 @@
 
         mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo());
 
-        verifyZeroInteractions(mWindowManager);
+        verify(mWindowManager, never()).addBlockScreenCaptureForApps(any());
     }
 
     @Test
@@ -422,7 +446,7 @@
 
         mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo());
 
-        verifyZeroInteractions(mWindowManager);
+        verify(mWindowManager, never()).addBlockScreenCaptureForApps(any());
     }
 
     @Test
@@ -435,7 +459,7 @@
 
         mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo());
 
-        verifyZeroInteractions(mWindowManager);
+        verify(mWindowManager, never()).addBlockScreenCaptureForApps(any());
     }
 
     @Test
diff --git a/services/tests/servicestests/src/com/android/server/appop/AppOpsActiveWatcherTest.java b/services/tests/servicestests/src/com/android/server/appop/AppOpsActiveWatcherTest.java
index b487dc6..c970a3e 100644
--- a/services/tests/servicestests/src/com/android/server/appop/AppOpsActiveWatcherTest.java
+++ b/services/tests/servicestests/src/com/android/server/appop/AppOpsActiveWatcherTest.java
@@ -16,7 +16,8 @@
 
 package com.android.server.appop;
 
-import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
+import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_CUSTOM;
+import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CAMERA;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -31,6 +32,7 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
 
+import android.Manifest;
 import android.app.AppOpsManager;
 import android.app.AppOpsManager.OnOpActiveChangedListener;
 import android.companion.virtual.VirtualDeviceManager;
@@ -38,7 +40,8 @@
 import android.content.AttributionSource;
 import android.content.Context;
 import android.os.Process;
-import android.virtualdevice.cts.common.FakeAssociationRule;
+import android.permission.PermissionManager;
+import android.virtualdevice.cts.common.VirtualDeviceRule;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
@@ -49,7 +52,6 @@
 import org.junit.runner.RunWith;
 
 import java.util.List;
-import java.util.concurrent.atomic.AtomicInteger;
 
 /**
  * Tests app ops version upgrades
@@ -59,7 +61,13 @@
 public class AppOpsActiveWatcherTest {
 
     @Rule
-    public FakeAssociationRule mFakeAssociationRule = new FakeAssociationRule();
+    public VirtualDeviceRule virtualDeviceRule =
+            VirtualDeviceRule.withAdditionalPermissions(
+                    Manifest.permission.GRANT_RUNTIME_PERMISSIONS,
+                    Manifest.permission.REVOKE_RUNTIME_PERMISSIONS,
+                    Manifest.permission.CREATE_VIRTUAL_DEVICE,
+                    Manifest.permission.GET_APP_OPS_STATS
+            );
     private static final long NOTIFICATION_TIMEOUT_MILLIS = 5000;
 
     @Test
@@ -145,20 +153,28 @@
 
     @Test
     public void testWatchActiveOpsForExternalDevice() {
-        final VirtualDeviceManager virtualDeviceManager = getContext().getSystemService(
-                VirtualDeviceManager.class);
-        AtomicInteger virtualDeviceId = new AtomicInteger();
-        runWithShellPermissionIdentity(() -> {
-            final VirtualDeviceManager.VirtualDevice virtualDevice =
-                    virtualDeviceManager.createVirtualDevice(
-                            mFakeAssociationRule.getAssociationInfo().getId(),
-                            new VirtualDeviceParams.Builder().setName("virtual_device").build());
-            virtualDeviceId.set(virtualDevice.getDeviceId());
-        });
+        VirtualDeviceManager.VirtualDevice virtualDevice =
+                virtualDeviceRule.createManagedVirtualDevice(
+                        new VirtualDeviceParams.Builder()
+                                .setDevicePolicy(POLICY_TYPE_CAMERA, DEVICE_POLICY_CUSTOM)
+                                .build()
+                );
+
+        PermissionManager permissionManager =
+                getContext().getSystemService(PermissionManager.class);
+
+        // Unlike runtime permission being automatically granted to the default device, we need to
+        // grant camera permission to the external device first before we can start op.
+        permissionManager.grantRuntimePermission(
+                getContext().getOpPackageName(),
+                Manifest.permission.CAMERA,
+                virtualDevice.getPersistentDeviceId()
+        );
+
         final OnOpActiveChangedListener listener = mock(OnOpActiveChangedListener.class);
         AttributionSource attributionSource = new AttributionSource(Process.myUid(),
                 getContext().getOpPackageName(), getContext().getAttributionTag(),
-                virtualDeviceId.get());
+                virtualDevice.getDeviceId());
 
         final AppOpsManager appOpsManager = getContext().getSystemService(AppOpsManager.class);
         appOpsManager.startWatchingActive(new String[]{AppOpsManager.OPSTR_CAMERA,
@@ -171,7 +187,7 @@
         verify(listener, timeout(NOTIFICATION_TIMEOUT_MILLIS)
                 .times(1)).onOpActiveChanged(eq(AppOpsManager.OPSTR_CAMERA),
                 eq(Process.myUid()), eq(getContext().getOpPackageName()),
-                eq(getContext().getAttributionTag()), eq(virtualDeviceId.get()), eq(true),
+                eq(getContext().getAttributionTag()), eq(virtualDevice.getDeviceId()), eq(true),
                 eq(AppOpsManager.ATTRIBUTION_FLAGS_NONE),
                 eq(AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE));
         verifyNoMoreInteractions(listener);
@@ -182,7 +198,7 @@
         verify(listener, timeout(NOTIFICATION_TIMEOUT_MILLIS)
                 .times(1)).onOpActiveChanged(eq(AppOpsManager.OPSTR_CAMERA),
                 eq(Process.myUid()), eq(getContext().getOpPackageName()),
-                eq(getContext().getAttributionTag()), eq(virtualDeviceId.get()), eq(false),
+                eq(getContext().getAttributionTag()), eq(virtualDevice.getDeviceId()), eq(false),
                 eq(AppOpsManager.ATTRIBUTION_FLAGS_NONE),
                 eq(AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE));
         verifyNoMoreInteractions(listener);
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskManagerServiceTests.java
index 400f9bb..4f94704 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskManagerServiceTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskManagerServiceTests.java
@@ -317,6 +317,70 @@
     }
 
     @Test
+    public void testEnterPipModeWhenResumed_autoEnterEnabled_returnTrue() {
+        final Task stack = new TaskBuilder(mSupervisor).setCreateActivity(true).build();
+        final ActivityRecord activity = stack.getBottomMostTask().getTopNonFinishingActivity();
+        PictureInPictureParams params = mock(PictureInPictureParams.class);
+        activity.pictureInPictureArgs = params;
+
+        doReturn(true).when(activity).isState(RESUMED);
+        doReturn(false).when(activity).inPinnedWindowingMode();
+        doReturn(true).when(activity).checkEnterPictureInPictureState(anyString(), anyBoolean());
+        doReturn(true).when(params).isAutoEnterEnabled();
+
+        assertTrue(mAtm.enterPictureInPictureMode(activity, params,
+                true /* fromClient */, true /* isAutoEnter */));
+    }
+
+    @Test
+    public void testEnterPipModeWhenResumed_autoEnterDisabled_returnTrue() {
+        final Task stack = new TaskBuilder(mSupervisor).setCreateActivity(true).build();
+        final ActivityRecord activity = stack.getBottomMostTask().getTopNonFinishingActivity();
+        PictureInPictureParams params = mock(PictureInPictureParams.class);
+        activity.pictureInPictureArgs = params;
+
+        doReturn(true).when(activity).isState(RESUMED);
+        doReturn(false).when(activity).inPinnedWindowingMode();
+        doReturn(true).when(activity).checkEnterPictureInPictureState(anyString(), anyBoolean());
+        doReturn(false).when(params).isAutoEnterEnabled();
+
+        assertTrue(mAtm.enterPictureInPictureMode(activity, params,
+                true /* fromClient */, false /* isAutoEnter */));
+    }
+
+    @Test
+    public void testEnterPipModeWhenPausing_autoEnterEnabled_returnFalse() {
+        final Task stack = new TaskBuilder(mSupervisor).setCreateActivity(true).build();
+        final ActivityRecord activity = stack.getBottomMostTask().getTopNonFinishingActivity();
+        PictureInPictureParams params = mock(PictureInPictureParams.class);
+        activity.pictureInPictureArgs = params;
+
+        doReturn(true).when(activity).isState(PAUSING);
+        doReturn(false).when(activity).inPinnedWindowingMode();
+        doReturn(true).when(activity).checkEnterPictureInPictureState(anyString(), anyBoolean());
+        doReturn(true).when(params).isAutoEnterEnabled();
+
+        assertFalse(mAtm.enterPictureInPictureMode(activity, params,
+                true /* fromClient */, true /* isAutoEnter */));
+    }
+
+    @Test
+    public void testEnterPipModeWhenPausing_autoEnterDisabled_returnTrue() {
+        final Task stack = new TaskBuilder(mSupervisor).setCreateActivity(true).build();
+        final ActivityRecord activity = stack.getBottomMostTask().getTopNonFinishingActivity();
+        PictureInPictureParams params = mock(PictureInPictureParams.class);
+        activity.pictureInPictureArgs = params;
+
+        doReturn(true).when(activity).isState(PAUSING);
+        doReturn(false).when(activity).inPinnedWindowingMode();
+        doReturn(true).when(activity).checkEnterPictureInPictureState(anyString(), anyBoolean());
+        doReturn(false).when(params).isAutoEnterEnabled();
+
+        assertTrue(mAtm.enterPictureInPictureMode(activity, params,
+                true /* fromClient */, false /* isAutoEnter */));
+    }
+
+    @Test
     public void testResumeNextActivityOnCrashedAppDied() {
         mSupervisor.beginDeferResume();
         final ActivityRecord homeActivity = new ActivityBuilder(mAtm)
diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
index 000162a..ac1dc08 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
@@ -4484,6 +4484,21 @@
     }
 
     @Test
+    public void testPortraitAppInTabletop_notSplitScreen() {
+        final int dw = 2400;
+        setUpDisplaySizeWithApp(dw, 2000);
+        prepareUnresizable(mActivity, SCREEN_ORIENTATION_PORTRAIT);
+
+        final int initialWidth = mActivity.getBounds().width();
+
+        setFoldablePosture(true /* isHalfFolded */, true /* isTabletop */);
+
+        final int finalWidth = mActivity.getBounds().width();
+        assertEquals(initialWidth, finalWidth);
+        assertNotEquals(finalWidth, getExpectedSplitSize(dw));
+    }
+
+    @Test
     public void testUpdateResolvedBoundsHorizontalPosition_bookModeEnabled() {
         // Set up a display in landscape with a fixed-orientation PORTRAIT app
         setUpDisplaySizeWithApp(2800, 1400);
diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java
index 87bb0f0..b005715 100644
--- a/telephony/java/android/telephony/satellite/SatelliteManager.java
+++ b/telephony/java/android/telephony/satellite/SatelliteManager.java
@@ -954,6 +954,11 @@
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public static final int SATELLITE_MODEM_STATE_CONNECTED = 7;
     /**
+     * The satellite modem is being powered on.
+     * @hide
+     */
+    public static final int SATELLITE_MODEM_STATE_ENABLING_SATELLITE = 8;
+    /**
      * Satellite modem state is unknown. This generic modem state should be used only when the
      * modem state cannot be mapped to other specific modem states.
      */
@@ -970,6 +975,7 @@
             SATELLITE_MODEM_STATE_UNAVAILABLE,
             SATELLITE_MODEM_STATE_NOT_CONNECTED,
             SATELLITE_MODEM_STATE_CONNECTED,
+            SATELLITE_MODEM_STATE_ENABLING_SATELLITE,
             SATELLITE_MODEM_STATE_UNKNOWN
     })
     @Retention(RetentionPolicy.SOURCE)
diff --git a/tests/FlickerTests/test-apps/app-helpers/OWNERS b/tests/FlickerTests/test-apps/app-helpers/OWNERS
new file mode 100644
index 0000000..ab62532
--- /dev/null
+++ b/tests/FlickerTests/test-apps/app-helpers/OWNERS
@@ -0,0 +1,2 @@
+uysalorhan@google.com
+pragyabajoria@google.com
\ No newline at end of file
diff --git a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt
new file mode 100644
index 0000000..461dfec
--- /dev/null
+++ b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm.flicker.helpers
+
+import android.tools.device.apphelpers.IStandardAppHelper
+import android.tools.helpers.SYSTEMUI_PACKAGE
+import android.tools.traces.parsers.WindowManagerStateHelper
+import android.tools.traces.wm.WindowingMode
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.BySelector
+import androidx.test.uiautomator.UiDevice
+import androidx.test.uiautomator.UiObject2
+import androidx.test.uiautomator.Until
+
+/**
+ * Wrapper class around App helper classes. This class adds functionality to the apps that the
+ * desktop apps would have.
+ */
+open class DesktopModeAppHelper(private val innerHelper: IStandardAppHelper) :
+    IStandardAppHelper by innerHelper {
+    private val TIMEOUT_MS = 3_000L
+    private val CAPTION = "desktop_mode_caption"
+    private val CAPTION_HANDLE = "caption_handle"
+    private val MAXIMIZE_BUTTON = "maximize_window"
+    private val MAXIMIZE_BUTTON_VIEW = "maximize_button_view"
+    private val CLOSE_BUTTON = "close_window"
+
+    private val caption: BySelector
+        get() = By.res(SYSTEMUI_PACKAGE, CAPTION)
+
+    /** Wait for an app moved to desktop to finish its transition. */
+    private fun waitForAppToMoveToDesktop(wmHelper: WindowManagerStateHelper) {
+        wmHelper
+            .StateSyncBuilder()
+            .withWindowSurfaceAppeared(innerHelper)
+            .withFreeformApp(innerHelper)
+            .withAppTransitionIdle()
+            .waitForAndVerify()
+    }
+
+    /** Move an app to Desktop by dragging the app handle at the top. */
+    fun enterDesktopWithDrag(
+        wmHelper: WindowManagerStateHelper,
+        device: UiDevice,
+    ) {
+        innerHelper.launchViaIntent(wmHelper)
+        dragToDesktop(wmHelper, device)
+        waitForAppToMoveToDesktop(wmHelper)
+    }
+
+    private fun dragToDesktop(wmHelper: WindowManagerStateHelper, device: UiDevice) {
+        val windowRect = wmHelper.getWindowRegion(innerHelper).bounds
+        val startX = windowRect.centerX()
+
+        // Start dragging a little under the top to prevent dragging the notification shade.
+        val startY = 10
+
+        val displayRect =
+            wmHelper.currentState.wmState.getDefaultDisplay()?.displayRect
+                ?: throw IllegalStateException("Default display is null")
+
+        // The position we want to drag to
+        val endY = displayRect.centerY() / 2
+
+        // drag the window to move to desktop
+        device.drag(startX, startY, startX, endY, 100)
+    }
+
+    /** Click maximise button on the app header for the given app. */
+    fun maximiseDesktopApp(wmHelper: WindowManagerStateHelper, device: UiDevice) {
+        val caption = getCaptionForTheApp(wmHelper, device)
+        val maximizeButton =
+            caption
+                ?.children
+                ?.find { it.resourceName.endsWith(MAXIMIZE_BUTTON_VIEW) }
+                ?.children
+                ?.get(0)
+        maximizeButton?.click()
+        wmHelper.StateSyncBuilder().withAppTransitionIdle().waitForAndVerify()
+    }
+    /** Click close button on the app header for the given app. */
+    fun closeDesktopApp(wmHelper: WindowManagerStateHelper, device: UiDevice) {
+        val caption = getCaptionForTheApp(wmHelper, device)
+        val closeButton = caption?.children?.find { it.resourceName.endsWith(CLOSE_BUTTON) }
+        closeButton?.click()
+        wmHelper
+            .StateSyncBuilder()
+            .withAppTransitionIdle()
+            .withWindowSurfaceDisappeared(innerHelper)
+            .waitForAndVerify()
+    }
+
+    private fun getCaptionForTheApp(
+        wmHelper: WindowManagerStateHelper,
+        device: UiDevice
+    ): UiObject2? {
+        if (
+            wmHelper.getWindow(innerHelper)?.windowingMode !=
+                WindowingMode.WINDOWING_MODE_FREEFORM.value
+        )
+            error("expected a freeform window with caption but window is not in freeform mode")
+        val captions =
+            device.wait(Until.findObjects(caption), TIMEOUT_MS)
+                ?: error("Unable to find view $caption\n")
+
+        return captions.find {
+            wmHelper.getWindowRegion(innerHelper).bounds.contains(it.visibleBounds)
+        }
+    }
+}