Merge "Move animation flows to immediate dispatcher" into main
diff --git a/apct-tests/perftests/core/src/android/text/VariableFontPerfTest.java b/apct-tests/perftests/core/src/android/text/VariableFontPerfTest.java
index fbe67a4..c34936f 100644
--- a/apct-tests/perftests/core/src/android/text/VariableFontPerfTest.java
+++ b/apct-tests/perftests/core/src/android/text/VariableFontPerfTest.java
@@ -19,6 +19,7 @@
 import android.graphics.Paint;
 import android.graphics.RecordingCanvas;
 import android.graphics.RenderNode;
+import android.graphics.Typeface;
 import android.perftests.utils.BenchmarkState;
 import android.perftests.utils.PerfStatusReporter;
 
@@ -120,13 +121,34 @@
     public void testSetFontVariationSettings() {
         final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         final Paint paint = new Paint(PAINT);
-        final Random random = new Random(0);
         while (state.keepRunning()) {
             state.pauseTiming();
-            int weight = random.nextInt(1000);
+            paint.setTypeface(null);
+            paint.setFontVariationSettings(null);
+            Typeface.clearTypefaceCachesForTestingPurpose();
             state.resumeTiming();
 
-            paint.setFontVariationSettings("'wght' " + weight);
+            paint.setFontVariationSettings("'wght' 450");
         }
+        Typeface.clearTypefaceCachesForTestingPurpose();
     }
+
+    @Test
+    public void testSetFontVariationSettings_Cached() {
+        final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+        final Paint paint = new Paint(PAINT);
+        Typeface.clearTypefaceCachesForTestingPurpose();
+
+        while (state.keepRunning()) {
+            state.pauseTiming();
+            paint.setTypeface(null);
+            paint.setFontVariationSettings(null);
+            state.resumeTiming();
+
+            paint.setFontVariationSettings("'wght' 450");
+        }
+
+        Typeface.clearTypefaceCachesForTestingPurpose();
+    }
+
 }
diff --git a/core/java/android/text/flags/flags.aconfig b/core/java/android/text/flags/flags.aconfig
index 3d190fe..5c5a2f6 100644
--- a/core/java/android/text/flags/flags.aconfig
+++ b/core/java/android/text/flags/flags.aconfig
@@ -267,3 +267,13 @@
     purpose: PURPOSE_BUGFIX
   }
 }
+
+flag {
+  name: "typeface_cache_for_var_settings"
+  namespace: "text"
+  description: "Cache Typeface instance for font variation settings."
+  bug: "355462362"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
\ No newline at end of file
diff --git a/core/java/android/view/contentprotection/OWNERS b/core/java/android/view/contentprotection/OWNERS
index b3583a7..48052c6 100644
--- a/core/java/android/view/contentprotection/OWNERS
+++ b/core/java/android/view/contentprotection/OWNERS
@@ -1,4 +1,6 @@
-# Bug component: 544200
+# Bug component: 1040349
 
-include /core/java/android/view/contentcapture/OWNERS
+njagar@google.com
+williamluh@google.com
+aaronjosephs@google.com
 
diff --git a/core/java/com/android/internal/protolog/LegacyProtoLogImpl.java b/core/java/com/android/internal/protolog/LegacyProtoLogImpl.java
index 572a599..fcc3023 100644
--- a/core/java/com/android/internal/protolog/LegacyProtoLogImpl.java
+++ b/core/java/com/android/internal/protolog/LegacyProtoLogImpl.java
@@ -48,6 +48,7 @@
 import java.io.IOException;
 import java.io.PrintWriter;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Map;
 import java.util.TreeMap;
 import java.util.stream.Collectors;
@@ -423,6 +424,14 @@
         for (IProtoLogGroup group : protoLogGroups) {
             mLogGroups.put(group.name(), group);
         }
+
+        final var hasGroupsLoggingToLogcat = Arrays.stream(protoLogGroups)
+                .anyMatch(IProtoLogGroup::isLogToLogcat);
+
+        final ILogger logger = (msg) -> Slog.i(TAG, msg);
+        if (hasGroupsLoggingToLogcat) {
+            mViewerConfig.loadViewerConfig(logger, mLegacyViewerConfigFilename);
+        }
     }
 }
 
diff --git a/core/res/res/drawable-car/car_activity_resolver_list_background.xml b/core/res/res/drawable-car/car_activity_resolver_list_background.xml
deleted file mode 100644
index dbbadd8..0000000
--- a/core/res/res/drawable-car/car_activity_resolver_list_background.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-<?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.
--->
-<shape xmlns:android="http://schemas.android.com/apk/res/android"
-    android:shape="rectangle">
-    <solid android:color="?attr/colorBackgroundFloating" />
-    <corners android:radius="@dimen/car_activity_resolver_corner_radius" />
-</shape>
\ No newline at end of file
diff --git a/core/res/res/layout-car/car_resolver_list.xml b/core/res/res/layout-car/car_resolver_list.xml
deleted file mode 100644
index 08c9861..0000000
--- a/core/res/res/layout-car/car_resolver_list.xml
+++ /dev/null
@@ -1,127 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
-* Copyright 2019, The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*     http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
--->
-<com.android.internal.widget.ResolverDrawerLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="@dimen/car_activity_resolver_width"
-    android:layout_height="wrap_content"
-    android:layout_gravity="center"
-    android:id="@id/contentPanel">
-
-    <LinearLayout
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:orientation="vertical"
-        android:background="@drawable/car_activity_resolver_list_background">
-
-        <LinearLayout
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:background="@drawable/car_activity_resolver_list_background"
-            android:orientation="horizontal"
-            android:paddingVertical="@dimen/car_padding_4"
-            android:paddingHorizontal="@dimen/car_padding_4" >
-            <TextView
-                android:id="@+id/profile_button"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:visibility="gone" />
-
-            <TextView
-                android:id="@+id/title"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:layout_gravity="start"
-                android:textAppearance="@android:style/TextAppearance.DeviceDefault.DialogWindowTitle" />
-        </LinearLayout>
-
-        <FrameLayout
-            android:id="@+id/stub"
-            android:visibility="gone"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"/>
-
-        <TabHost
-            android:id="@+id/profile_tabhost"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:layout_alignParentTop="true"
-            android:layout_centerHorizontal="true"
-            android:background="?android:attr/colorBackgroundFloating">
-            <LinearLayout
-                android:orientation="vertical"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content">
-                <TabWidget
-                    android:id="@android:id/tabs"
-                    android:visibility="gone"
-                    android:layout_width="match_parent"
-                    android:layout_height="wrap_content">
-                </TabWidget>
-                <View
-                    android:id="@+id/resolver_tab_divider"
-                    android:visibility="gone"
-                    android:layout_width="match_parent"
-                    android:layout_height="wrap_content" />
-                <FrameLayout
-                    android:id="@android:id/tabcontent"
-                    android:layout_width="match_parent"
-                    android:layout_height="wrap_content">
-                    <com.android.internal.app.ResolverViewPager
-                        android:id="@+id/profile_pager"
-                        android:layout_width="match_parent"
-                        android:layout_height="wrap_content"/>
-                </FrameLayout>
-            </LinearLayout>
-        </TabHost>
-
-        <LinearLayout
-            android:id="@+id/button_bar"
-            android:visibility="gone"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:layout_marginVertical="@dimen/car_padding_4"
-            android:layout_marginHorizontal="@dimen/car_padding_4"
-            android:padding="0dp"
-            android:gravity="center"
-            android:background="@drawable/car_activity_resolver_list_background"
-            android:orientation="vertical">
-
-            <Button
-                android:id="@+id/button_once"
-                android:layout_width="match_parent"
-                android:layout_height="@dimen/car_button_height"
-                android:enabled="false"
-                android:layout_gravity="center"
-                android:layout_marginBottom="@dimen/car_padding_2"
-                android:text="@string/activity_resolver_use_once"
-                android:onClick="onButtonClick"/>
-
-            <Button
-                android:id="@+id/button_always"
-                android:layout_width="match_parent"
-                android:layout_height="@dimen/car_button_height"
-                android:enabled="false"
-                android:layout_gravity="center"
-                android:text="@string/activity_resolver_use_always"
-                android:onClick="onButtonClick"/>
-        </LinearLayout>
-
-    </LinearLayout>
-
-</com.android.internal.widget.ResolverDrawerLayout>
\ No newline at end of file
diff --git a/core/res/res/layout-car/car_resolver_list_with_default.xml b/core/res/res/layout-car/car_resolver_list_with_default.xml
deleted file mode 100644
index 08cc7ff..0000000
--- a/core/res/res/layout-car/car_resolver_list_with_default.xml
+++ /dev/null
@@ -1,159 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
-* Copyright 2019, The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*     http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
--->
-<com.android.internal.widget.ResolverDrawerLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="@dimen/car_activity_resolver_width"
-    android:layout_height="wrap_content"
-    android:layout_gravity="center"
-    android:id="@id/contentPanel">
-
-    <LinearLayout
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:orientation="vertical"
-        android:layout_gravity="center"
-        android:background="@drawable/car_activity_resolver_list_background">
-
-        <FrameLayout
-            android:id="@+id/stub"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:background="@drawable/car_activity_resolver_list_background"/>
-
-
-        <LinearLayout
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:minHeight="@dimen/car_activity_resolver_list_item_height"
-            android:orientation="horizontal">
-
-            <RadioButton
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:focusable="false"
-                android:clickable="false"
-                android:layout_marginStart="?attr/listPreferredItemPaddingStart"
-                android:layout_gravity="start|center_vertical"
-                android:checked="true"/>
-
-            <ImageView
-                android:id="@+id/icon"
-                android:layout_width="@dimen/car_icon_size"
-                android:layout_height="@dimen/car_icon_size"
-                android:layout_gravity="start|center_vertical"
-                android:layout_marginStart="@dimen/car_padding_4"
-                android:src="@drawable/resolver_icon_placeholder"
-                android:scaleType="fitCenter"/>
-
-            <TextView
-                android:id="@+id/title"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:layout_marginHorizontal="?attr/listPreferredItemPaddingStart"
-                style="?android:attr/textAppearanceListItem"
-                android:layout_gravity="start|center_vertical" />
-
-            <LinearLayout
-                android:id="@+id/profile_button"
-                android:visibility="gone"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content">
-
-                <ImageView
-                    android:id="@+id/icon"
-                    android:visibility="gone"
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content" />
-
-                <TextView
-                    android:id="@id/text1"
-                    android:visibility="gone"
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content" />
-            </LinearLayout>
-        </LinearLayout>
-
-        <TabHost
-            android:id="@+id/profile_tabhost"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:layout_alignParentTop="true"
-            android:layout_centerHorizontal="true"
-            android:background="?attr/colorBackgroundFloating">
-            <LinearLayout
-                android:orientation="vertical"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content">
-                <TabWidget
-                    android:id="@android:id/tabs"
-                    android:layout_width="match_parent"
-                    android:layout_height="wrap_content"
-                    android:visibility="gone">
-                </TabWidget>
-                <View
-                    android:id="@+id/resolver_tab_divider"
-                    android:visibility="gone"
-                    android:layout_width="match_parent"
-                    android:layout_height="wrap_content" />
-                <FrameLayout
-                    android:id="@android:id/tabcontent"
-                    android:layout_width="match_parent"
-                    android:layout_height="wrap_content">
-                    <com.android.internal.app.ResolverViewPager
-                        android:id="@+id/profile_pager"
-                        android:layout_width="match_parent"
-                        android:layout_height="wrap_content">
-                    </com.android.internal.app.ResolverViewPager>
-                </FrameLayout>
-            </LinearLayout>
-        </TabHost>
-
-        <LinearLayout
-            android:id="@+id/button_bar"
-            android:visibility="gone"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:layout_marginVertical="@dimen/car_padding_4"
-            android:layout_marginHorizontal="@dimen/car_padding_4"
-            android:gravity="center"
-            android:background="@drawable/car_activity_resolver_list_background"
-            android:orientation="vertical">
-
-            <Button
-                android:id="@+id/button_once"
-                android:layout_width="match_parent"
-                android:layout_height="@dimen/car_button_height"
-                android:enabled="false"
-                android:layout_gravity="center"
-                android:layout_marginBottom="@dimen/car_padding_2"
-                android:text="@string/activity_resolver_use_once"
-                android:onClick="onButtonClick"/>
-
-            <Button
-                android:id="@+id/button_always"
-                android:layout_width="match_parent"
-                android:layout_height="@dimen/car_button_height"
-                android:enabled="false"
-                android:layout_gravity="center"
-                android:text="@string/activity_resolver_use_always"
-                android:onClick="onButtonClick"/>
-        </LinearLayout>
-    </LinearLayout>
-
-</com.android.internal.widget.ResolverDrawerLayout>
diff --git a/core/tests/coretests/src/android/graphics/PaintFontVariationTest.java b/core/tests/coretests/src/android/graphics/PaintFontVariationTest.java
new file mode 100644
index 0000000..8a54e5b
--- /dev/null
+++ b/core/tests/coretests/src/android/graphics/PaintFontVariationTest.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2015 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.graphics;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.test.InstrumentationTestCase;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.text.flags.Flags;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * PaintTest tests {@link Paint}.
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class PaintFontVariationTest extends InstrumentationTestCase {
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
+    @RequiresFlagsEnabled(Flags.FLAG_TYPEFACE_CACHE_FOR_VAR_SETTINGS)
+    @Test
+    public void testDerivedFromSameTypeface() {
+        final Paint p = new Paint();
+
+        p.setTypeface(Typeface.SANS_SERIF);
+        assertThat(p.setFontVariationSettings("'wght' 450")).isTrue();
+        Typeface first = p.getTypeface();
+
+        p.setTypeface(Typeface.SANS_SERIF);
+        assertThat(p.setFontVariationSettings("'wght' 480")).isTrue();
+        Typeface second = p.getTypeface();
+
+        assertThat(first.getDerivedFrom()).isSameInstanceAs(second.getDerivedFrom());
+    }
+
+    @RequiresFlagsEnabled(Flags.FLAG_TYPEFACE_CACHE_FOR_VAR_SETTINGS)
+    @Test
+    public void testDerivedFromChained() {
+        final Paint p = new Paint();
+
+        p.setTypeface(Typeface.SANS_SERIF);
+        assertThat(p.setFontVariationSettings("'wght' 450")).isTrue();
+        Typeface first = p.getTypeface();
+
+        assertThat(p.setFontVariationSettings("'wght' 480")).isTrue();
+        Typeface second = p.getTypeface();
+
+        assertThat(first.getDerivedFrom()).isSameInstanceAs(second.getDerivedFrom());
+    }
+}
diff --git a/core/tests/coretests/src/android/graphics/PaintTest.java b/core/tests/coretests/src/android/graphics/PaintTest.java
index 0dec756..878ba70 100644
--- a/core/tests/coretests/src/android/graphics/PaintTest.java
+++ b/core/tests/coretests/src/android/graphics/PaintTest.java
@@ -16,13 +16,22 @@
 
 package android.graphics;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.junit.Assert.assertNotEquals;
 
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.test.InstrumentationTestCase;
 import android.text.TextUtils;
 
 import androidx.test.filters.SmallTest;
 
+import com.android.text.flags.Flags;
+
+import org.junit.Rule;
+
 import java.util.Arrays;
 import java.util.HashSet;
 
@@ -30,6 +39,9 @@
  * PaintTest tests {@link Paint}.
  */
 public class PaintTest extends InstrumentationTestCase {
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
     private static final String FONT_PATH = "fonts/HintedAdvanceWidthTest-Regular.ttf";
 
     static void assertEquals(String message, float[] expected, float[] actual) {
@@ -403,4 +415,33 @@
         assertEquals(6, getClusterCount(p, rtlStr + ltrStr));
         assertEquals(9, getClusterCount(p, ltrStr + rtlStr + ltrStr));
     }
+
+    @RequiresFlagsEnabled(Flags.FLAG_TYPEFACE_CACHE_FOR_VAR_SETTINGS)
+    public void testDerivedFromSameTypeface() {
+        final Paint p = new Paint();
+
+        p.setTypeface(Typeface.SANS_SERIF);
+        assertThat(p.setFontVariationSettings("'wght' 450")).isTrue();
+        Typeface first = p.getTypeface();
+
+        p.setTypeface(Typeface.SANS_SERIF);
+        assertThat(p.setFontVariationSettings("'wght' 480")).isTrue();
+        Typeface second = p.getTypeface();
+
+        assertThat(first.getDerivedFrom()).isSameInstanceAs(second.getDerivedFrom());
+    }
+
+    @RequiresFlagsEnabled(Flags.FLAG_TYPEFACE_CACHE_FOR_VAR_SETTINGS)
+    public void testDerivedFromChained() {
+        final Paint p = new Paint();
+
+        p.setTypeface(Typeface.SANS_SERIF);
+        assertThat(p.setFontVariationSettings("'wght' 450")).isTrue();
+        Typeface first = p.getTypeface();
+
+        assertThat(p.setFontVariationSettings("'wght' 480")).isTrue();
+        Typeface second = p.getTypeface();
+
+        assertThat(first.getDerivedFrom()).isSameInstanceAs(second.getDerivedFrom());
+    }
 }
diff --git a/core/tests/coretests/src/android/view/contentprotection/OWNERS b/core/tests/coretests/src/android/view/contentprotection/OWNERS
index b3583a7..3d09da3 100644
--- a/core/tests/coretests/src/android/view/contentprotection/OWNERS
+++ b/core/tests/coretests/src/android/view/contentprotection/OWNERS
@@ -1,4 +1,4 @@
-# Bug component: 544200
+# Bug component: 1040349
 
-include /core/java/android/view/contentcapture/OWNERS
+include /core/java/android/view/contentprotection/OWNERS
 
diff --git a/graphics/java/android/graphics/Typeface.java b/graphics/java/android/graphics/Typeface.java
index fd78816..889a7785 100644
--- a/graphics/java/android/graphics/Typeface.java
+++ b/graphics/java/android/graphics/Typeface.java
@@ -56,6 +56,7 @@
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.Preconditions;
+import com.android.text.flags.Flags;
 
 import dalvik.annotation.optimization.CriticalNative;
 import dalvik.annotation.optimization.FastNative;
@@ -74,6 +75,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -143,6 +145,23 @@
     private static final LruCache<String, Typeface> sDynamicTypefaceCache = new LruCache<>(16);
     private static final Object sDynamicCacheLock = new Object();
 
+    private static final LruCache<Long, LruCache<String, Typeface>> sVariableCache =
+            new LruCache<>(16);
+    private static final Object sVariableCacheLock = new Object();
+
+    /** @hide */
+    @VisibleForTesting
+    public static void clearTypefaceCachesForTestingPurpose() {
+        synchronized (sWeightCacheLock) {
+            sWeightTypefaceCache.clear();
+        }
+        synchronized (sDynamicCacheLock) {
+            sDynamicTypefaceCache.evictAll();
+        }
+        synchronized (sVariableCacheLock) {
+            sVariableCache.evictAll();
+        }
+    }
 
     @GuardedBy("SYSTEM_FONT_MAP_LOCK")
     static Typeface sDefaultTypeface;
@@ -195,6 +214,8 @@
     @UnsupportedAppUsage
     public final long native_instance;
 
+    private final Typeface mDerivedFrom;
+
     private final String mSystemFontFamilyName;
 
     private final Runnable mCleaner;
@@ -274,6 +295,18 @@
     }
 
     /**
+     * Returns the Typeface used for creating this Typeface.
+     *
+     * Maybe null if this is not derived from other Typeface.
+     * TODO(b/357707916): Make this public API.
+     * @hide
+     */
+    @VisibleForTesting
+    public final @Nullable Typeface getDerivedFrom() {
+        return mDerivedFrom;
+    }
+
+    /**
      * Returns the system font family name if the typeface was created from a system font family,
      * otherwise returns null.
      */
@@ -1021,9 +1054,51 @@
         return typeface;
     }
 
-    /** @hide */
+    private static String axesToVarKey(@NonNull List<FontVariationAxis> axes) {
+        // The given list can be mutated because it is allocated in Paint#setFontVariationSettings.
+        // Currently, Paint#setFontVariationSettings is the only code path reaches this method.
+        axes.sort(Comparator.comparingInt(FontVariationAxis::getOpenTypeTagValue));
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < axes.size(); ++i) {
+            final FontVariationAxis fva = axes.get(i);
+            sb.append(fva.getTag());
+            sb.append(fva.getStyleValue());
+        }
+        return sb.toString();
+    }
+
+    /**
+     * TODO(b/357707916): Make this public API.
+     * @hide
+     */
     public static Typeface createFromTypefaceWithVariation(@Nullable Typeface family,
             @NonNull List<FontVariationAxis> axes) {
+        if (Flags.typefaceCacheForVarSettings()) {
+            final Typeface target = (family == null) ? Typeface.DEFAULT : family;
+            final Typeface base = (target.mDerivedFrom == null) ? target : target.mDerivedFrom;
+
+            final String key = axesToVarKey(axes);
+
+            synchronized (sVariableCacheLock) {
+                LruCache<String, Typeface> innerCache = sVariableCache.get(base.native_instance);
+                if (innerCache == null) {
+                    // Cache up to 16 var instance per root Typeface
+                    innerCache = new LruCache<>(16);
+                    sVariableCache.put(base.native_instance, innerCache);
+                } else {
+                    Typeface cached = innerCache.get(key);
+                    if (cached != null) {
+                        return cached;
+                    }
+                }
+                Typeface typeface = new Typeface(
+                        nativeCreateFromTypefaceWithVariation(base.native_instance, axes),
+                        base.getSystemFontFamilyName(), base);
+                innerCache.put(key, typeface);
+                return typeface;
+            }
+        }
+
         final Typeface base = family == null ? Typeface.DEFAULT : family;
         Typeface typeface = new Typeface(
                 nativeCreateFromTypefaceWithVariation(base.native_instance, axes),
@@ -1184,11 +1259,19 @@
     // don't allow clients to call this directly
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
     private Typeface(long ni) {
-        this(ni, null);
+        this(ni, null, null);
+    }
+
+
+    // don't allow clients to call this directly
+    // This is kept for robolectric.
+    private Typeface(long ni, @Nullable String systemFontFamilyName) {
+        this(ni, systemFontFamilyName, null);
     }
 
     // don't allow clients to call this directly
-    private Typeface(long ni, @Nullable String systemFontFamilyName) {
+    private Typeface(long ni, @Nullable String systemFontFamilyName,
+            @Nullable Typeface derivedFrom) {
         if (ni == 0) {
             throw new RuntimeException("native typeface cannot be made");
         }
@@ -1198,6 +1281,7 @@
         mStyle = nativeGetStyle(ni);
         mWeight = nativeGetWeight(ni);
         mSystemFontFamilyName = systemFontFamilyName;
+        mDerivedFrom = derivedFrom;
     }
 
     /**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
index 456d92a..544c2dd 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
@@ -1108,16 +1108,21 @@
     /** Handle task closing by removing wallpaper activity if it's the last active task */
     private fun handleTaskClosing(task: RunningTaskInfo): WindowContainerTransaction? {
         logV("handleTaskClosing")
+        if (!isDesktopModeShowing(task.displayId))
+            return null
+
         val wct = WindowContainerTransaction()
         if (taskRepository.isOnlyVisibleNonClosingTask(task.taskId)
-            && taskRepository.wallpaperActivityToken != null) {
+            && taskRepository.wallpaperActivityToken != null
+        ) {
             // Remove wallpaper activity when the last active task is removed
             removeWallpaperActivity(wct)
         }
         taskRepository.addClosingTask(task.displayId, task.taskId)
         // If a CLOSE or TO_BACK is triggered on a desktop task, remove the task.
         if (DesktopModeFlags.BACK_NAVIGATION.isEnabled(context) &&
-            taskRepository.isVisibleTask(task.taskId)) {
+            taskRepository.isVisibleTask(task.taskId)
+        ) {
             wct.removeTask(task.token)
         }
         return if (wct.isEmpty) null else wct
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
index 18c6228..a630cef 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
@@ -1819,6 +1819,19 @@
   }
 
   @Test
+  @EnableFlags(
+    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
+  )
+  fun handleRequest_backTransition_singleTaskNoToken_withWallpaper_notInDesktop_doesNotHandle() {
+    val task = setUpFreeformTask()
+    markTaskHidden(task)
+
+    val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK))
+
+    assertNull(result, "Should not handle request")
+  }
+
+  @Test
   @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
   @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
   fun handleRequest_backTransition_singleTaskNoToken_noBackNav_doesNotHandle() {
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 66b9e5c..5632e30 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -991,6 +991,16 @@
 }
 
 flag {
+  name: "communal_widget_trampoline_fix"
+  namespace: "systemui"
+  description: "fixes activity starts caused by non-activity trampolines from widgets."
+  bug: "350468769"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
+
+flag {
   name: "app_clips_backlinks"
   namespace: "systemui"
   description: "Enables Backlinks improvement feature in App Clips"
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt
index f655ac1..d164eab 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt
@@ -95,7 +95,7 @@
 import com.android.systemui.bouncer.ui.helper.BouncerSceneLayout
 import com.android.systemui.bouncer.ui.viewmodel.AuthMethodBouncerViewModel
 import com.android.systemui.bouncer.ui.viewmodel.BouncerMessageViewModel
-import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel
+import com.android.systemui.bouncer.ui.viewmodel.BouncerSceneContentViewModel
 import com.android.systemui.bouncer.ui.viewmodel.MessageViewModel
 import com.android.systemui.bouncer.ui.viewmodel.PasswordBouncerViewModel
 import com.android.systemui.bouncer.ui.viewmodel.PatternBouncerViewModel
@@ -114,7 +114,7 @@
 
 @Composable
 fun BouncerContent(
-    viewModel: BouncerViewModel,
+    viewModel: BouncerSceneContentViewModel,
     dialogFactory: BouncerDialogFactory,
     modifier: Modifier = Modifier,
 ) {
@@ -128,7 +128,7 @@
 @VisibleForTesting
 fun BouncerContent(
     layout: BouncerSceneLayout,
-    viewModel: BouncerViewModel,
+    viewModel: BouncerSceneContentViewModel,
     dialogFactory: BouncerDialogFactory,
     modifier: Modifier
 ) {
@@ -173,7 +173,7 @@
  */
 @Composable
 private fun StandardLayout(
-    viewModel: BouncerViewModel,
+    viewModel: BouncerSceneContentViewModel,
     modifier: Modifier = Modifier,
 ) {
     val isHeightExpanded =
@@ -235,7 +235,7 @@
  */
 @Composable
 private fun SplitLayout(
-    viewModel: BouncerViewModel,
+    viewModel: BouncerSceneContentViewModel,
     modifier: Modifier = Modifier,
 ) {
     val authMethod by viewModel.authMethodViewModel.collectAsStateWithLifecycle()
@@ -326,7 +326,7 @@
  */
 @Composable
 private fun BesideUserSwitcherLayout(
-    viewModel: BouncerViewModel,
+    viewModel: BouncerSceneContentViewModel,
     modifier: Modifier = Modifier,
 ) {
     val layoutDirection = LocalLayoutDirection.current
@@ -461,7 +461,7 @@
 /** Arranges the bouncer contents and user switcher contents one on top of the other, vertically. */
 @Composable
 private fun BelowUserSwitcherLayout(
-    viewModel: BouncerViewModel,
+    viewModel: BouncerSceneContentViewModel,
     modifier: Modifier = Modifier,
 ) {
     Column(
@@ -506,7 +506,7 @@
 
 @Composable
 private fun FoldAware(
-    viewModel: BouncerViewModel,
+    viewModel: BouncerSceneContentViewModel,
     aboveFold: @Composable BoxScope.() -> Unit,
     belowFold: @Composable BoxScope.() -> Unit,
     modifier: Modifier = Modifier,
@@ -649,7 +649,7 @@
  */
 @Composable
 private fun OutputArea(
-    viewModel: BouncerViewModel,
+    viewModel: BouncerSceneContentViewModel,
     modifier: Modifier = Modifier,
 ) {
     val authMethodViewModel: AuthMethodBouncerViewModel? by
@@ -677,7 +677,7 @@
  */
 @Composable
 private fun InputArea(
-    viewModel: BouncerViewModel,
+    viewModel: BouncerSceneContentViewModel,
     pinButtonRowVerticalSpacing: Dp,
     centerPatternDotsVertically: Boolean,
     modifier: Modifier = Modifier,
@@ -706,7 +706,7 @@
 
 @Composable
 private fun ActionArea(
-    viewModel: BouncerViewModel,
+    viewModel: BouncerSceneContentViewModel,
     modifier: Modifier = Modifier,
 ) {
     val actionButton: BouncerActionButtonModel? by
@@ -774,7 +774,7 @@
 
 @Composable
 private fun Dialog(
-    bouncerViewModel: BouncerViewModel,
+    bouncerViewModel: BouncerSceneContentViewModel,
     dialogFactory: BouncerDialogFactory,
 ) {
     val dialogViewModel by bouncerViewModel.dialogViewModel.collectAsStateWithLifecycle()
@@ -803,7 +803,7 @@
 /** Renders the UI of the user switcher that's displayed on large screens next to the bouncer UI. */
 @Composable
 private fun UserSwitcher(
-    viewModel: BouncerViewModel,
+    viewModel: BouncerSceneContentViewModel,
     modifier: Modifier = Modifier,
 ) {
     if (!viewModel.isUserSwitcherVisible) {
@@ -884,7 +884,7 @@
 @Composable
 private fun UserSwitcherDropdownMenu(
     isExpanded: Boolean,
-    items: List<BouncerViewModel.UserSwitcherDropdownItemViewModel>,
+    items: List<BouncerSceneContentViewModel.UserSwitcherDropdownItemViewModel>,
     onDismissed: () -> Unit,
 ) {
     val context = LocalContext.current
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt
index 9fd30b4..3a46882 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt
@@ -27,9 +27,11 @@
 import com.android.compose.animation.scene.UserAction
 import com.android.compose.animation.scene.UserActionResult
 import com.android.systemui.bouncer.ui.BouncerDialogFactory
-import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel
+import com.android.systemui.bouncer.ui.viewmodel.BouncerSceneActionsViewModel
+import com.android.systemui.bouncer.ui.viewmodel.BouncerSceneContentViewModel
 import com.android.systemui.compose.modifiers.sysuiResTag
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.lifecycle.rememberViewModel
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.scene.ui.composable.ComposableScene
 import javax.inject.Inject
@@ -51,23 +53,37 @@
 class BouncerScene
 @Inject
 constructor(
-    private val viewModel: BouncerViewModel,
+    private val actionsViewModelFactory: BouncerSceneActionsViewModel.Factory,
+    private val contentViewModelFactory: BouncerSceneContentViewModel.Factory,
     private val dialogFactory: BouncerDialogFactory,
 ) : ComposableScene {
     override val key = Scenes.Bouncer
 
+    private val actionsViewModel: BouncerSceneActionsViewModel by lazy {
+        actionsViewModelFactory.create()
+    }
+
     override val destinationScenes: Flow<Map<UserAction, UserActionResult>> =
-        viewModel.destinationScenes
+        actionsViewModel.actions
+
+    override suspend fun activate() {
+        actionsViewModel.activate()
+    }
 
     @Composable
     override fun SceneScope.Content(
         modifier: Modifier,
-    ) = BouncerScene(viewModel, dialogFactory, modifier)
+    ) =
+        BouncerScene(
+            viewModel = rememberViewModel { contentViewModelFactory.create() },
+            dialogFactory = dialogFactory,
+            modifier = modifier,
+        )
 }
 
 @Composable
 private fun SceneScope.BouncerScene(
-    viewModel: BouncerViewModel,
+    viewModel: BouncerSceneContentViewModel,
     dialogFactory: BouncerDialogFactory,
     modifier: Modifier = Modifier,
 ) {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt
index c9fa671..deef652 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt
@@ -22,14 +22,14 @@
 import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository
 import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
-import com.android.systemui.bouncer.domain.interactor.bouncerInteractor
-import com.android.systemui.bouncer.domain.interactor.simBouncerInteractor
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.kosmos.testScope
+import com.android.systemui.lifecycle.activateIn
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.test.runTest
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -39,17 +39,16 @@
 
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
-    private val bouncerInteractor by lazy { kosmos.bouncerInteractor }
-    private val underTest by lazy {
-        PinBouncerViewModel(
-            applicationContext = context,
-            viewModelScope = testScope.backgroundScope,
-            interactor = bouncerInteractor,
+    private val underTest =
+        kosmos.pinBouncerViewModelFactory.create(
             isInputEnabled = MutableStateFlow(true),
-            simBouncerInteractor = kosmos.simBouncerInteractor,
-            authenticationMethod = AuthenticationMethodModel.Pin,
             onIntentionalUserInput = {},
+            authenticationMethod = AuthenticationMethodModel.Pin,
         )
+
+    @Before
+    fun setUp() {
+        underTest.activateIn(testScope)
     }
 
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelTest.kt
index 4f5d0e5..b83ab7e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelTest.kt
@@ -52,6 +52,7 @@
 import com.android.systemui.keyguard.shared.model.FailFingerprintAuthenticationStatus
 import com.android.systemui.keyguard.shared.model.HelpFingerprintAuthenticationStatus
 import com.android.systemui.kosmos.testScope
+import com.android.systemui.lifecycle.activateIn
 import com.android.systemui.res.R
 import com.android.systemui.testKosmos
 import com.android.systemui.user.data.repository.fakeUserRepository
@@ -87,6 +88,7 @@
             intArrayOf(ignoreHelpMessageId)
         )
         underTest = kosmos.bouncerMessageViewModel
+        underTest.activateIn(testScope)
         overrideResource(R.string.kg_trust_agent_disabled, "Trust agent is unavailable")
         kosmos.fakeSystemPropertiesHelper.set(
             DeviceUnlockedInteractor.SYS_BOOT_REASON_PROP,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneActionsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneActionsViewModelTest.kt
new file mode 100644
index 0000000..a86a0c0
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneActionsViewModelTest.kt
@@ -0,0 +1,77 @@
+/*
+ * 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.bouncer.ui.viewmodel
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.compose.animation.scene.Back
+import com.android.compose.animation.scene.Swipe
+import com.android.compose.animation.scene.SwipeDirection
+import com.android.compose.animation.scene.UserActionResult
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.flags.EnableSceneContainer
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.lifecycle.activateIn
+import com.android.systemui.scene.domain.startable.sceneContainerStartable
+import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.scene.shared.model.fakeSceneDataSource
+import com.android.systemui.testKosmos
+import com.android.systemui.truth.containsEntriesExactly
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@EnableSceneContainer
+class BouncerSceneActionsViewModelTest : SysuiTestCase() {
+
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+
+    private lateinit var underTest: BouncerSceneActionsViewModel
+
+    @Before
+    fun setUp() {
+        kosmos.sceneContainerStartable.start()
+        underTest = kosmos.bouncerSceneActionsViewModel
+        underTest.activateIn(testScope)
+    }
+
+    @Test
+    fun actions() =
+        testScope.runTest {
+            val actions by collectLastValue(underTest.actions)
+            kosmos.fakeSceneDataSource.changeScene(Scenes.QuickSettings)
+            runCurrent()
+
+            kosmos.fakeSceneDataSource.changeScene(Scenes.Bouncer)
+            runCurrent()
+
+            assertThat(actions)
+                .containsEntriesExactly(
+                    Back to UserActionResult(Scenes.QuickSettings),
+                    Swipe(SwipeDirection.Down) to UserActionResult(Scenes.QuickSettings),
+                )
+        }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneContentViewModelTest.kt
similarity index 88%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneContentViewModelTest.kt
index ccddc9c..9bddcd2 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneContentViewModelTest.kt
@@ -18,10 +18,6 @@
 
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import com.android.compose.animation.scene.Back
-import com.android.compose.animation.scene.Swipe
-import com.android.compose.animation.scene.SwipeDirection
-import com.android.compose.animation.scene.UserActionResult
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository
 import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository
@@ -38,11 +34,9 @@
 import com.android.systemui.flags.Flags
 import com.android.systemui.flags.fakeFeatureFlagsClassic
 import com.android.systemui.kosmos.testScope
+import com.android.systemui.lifecycle.activateIn
 import com.android.systemui.scene.domain.startable.sceneContainerStartable
-import com.android.systemui.scene.shared.model.Scenes
-import com.android.systemui.scene.shared.model.fakeSceneDataSource
 import com.android.systemui.testKosmos
-import com.android.systemui.truth.containsEntriesExactly
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -62,17 +56,18 @@
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 @EnableSceneContainer
-class BouncerViewModelTest : SysuiTestCase() {
+class BouncerSceneContentViewModelTest : SysuiTestCase() {
 
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
 
-    private lateinit var underTest: BouncerViewModel
+    private lateinit var underTest: BouncerSceneContentViewModel
 
     @Before
     fun setUp() {
         kosmos.sceneContainerStartable.start()
-        underTest = kosmos.bouncerViewModel
+        underTest = kosmos.bouncerSceneContentViewModel
+        underTest.activateIn(testScope)
     }
 
     @Test
@@ -201,23 +196,6 @@
             assertThat(isFoldSplitRequired).isTrue()
         }
 
-    @Test
-    fun destinationScenes() =
-        testScope.runTest {
-            val destinationScenes by collectLastValue(underTest.destinationScenes)
-            kosmos.fakeSceneDataSource.changeScene(Scenes.QuickSettings)
-            runCurrent()
-
-            kosmos.fakeSceneDataSource.changeScene(Scenes.Bouncer)
-            runCurrent()
-
-            assertThat(destinationScenes)
-                .containsEntriesExactly(
-                    Back to UserActionResult(Scenes.QuickSettings),
-                    Swipe(SwipeDirection.Down) to UserActionResult(Scenes.QuickSettings),
-                )
-        }
-
     private fun authMethodsToTest(): List<AuthenticationMethodModel> {
         return listOf(None, Pin, Password, Pattern, Sim)
     }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
index a09189e..492543f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
@@ -31,6 +31,7 @@
 import com.android.systemui.inputmethod.data.repository.fakeInputMethodRepository
 import com.android.systemui.inputmethod.domain.interactor.inputMethodInteractor
 import com.android.systemui.kosmos.testScope
+import com.android.systemui.lifecycle.activateIn
 import com.android.systemui.res.R
 import com.android.systemui.scene.domain.interactor.sceneInteractor
 import com.android.systemui.scene.shared.model.Scenes
@@ -44,7 +45,6 @@
 import kotlin.time.Duration.Companion.seconds
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.advanceTimeBy
 import kotlinx.coroutines.test.runCurrent
@@ -68,12 +68,8 @@
     private val isInputEnabled = MutableStateFlow(true)
 
     private val underTest =
-        PasswordBouncerViewModel(
-            viewModelScope = testScope.backgroundScope,
-            isInputEnabled = isInputEnabled.asStateFlow(),
-            interactor = bouncerInteractor,
-            inputMethodInteractor = inputMethodInteractor,
-            selectedUserInteractor = selectedUserInteractor,
+        kosmos.passwordBouncerViewModelFactory.create(
+            isInputEnabled = isInputEnabled,
             onIntentionalUserInput = {},
         )
 
@@ -81,6 +77,7 @@
     fun setUp() {
         overrideResource(R.string.keyguard_enter_your_password, ENTER_YOUR_PASSWORD)
         overrideResource(R.string.kg_wrong_password, WRONG_PASSWORD)
+        underTest.activateIn(testScope)
     }
 
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
index 14d3634..7c773a9 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
@@ -26,9 +26,9 @@
 import com.android.systemui.authentication.domain.interactor.authenticationInteractor
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.authentication.shared.model.AuthenticationPatternCoordinate as Point
-import com.android.systemui.bouncer.domain.interactor.bouncerInteractor
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.kosmos.testScope
+import com.android.systemui.lifecycle.activateIn
 import com.android.systemui.res.R
 import com.android.systemui.scene.domain.interactor.sceneInteractor
 import com.android.systemui.scene.shared.model.Scenes
@@ -54,17 +54,12 @@
     private val testScope = kosmos.testScope
     private val authenticationInteractor by lazy { kosmos.authenticationInteractor }
     private val sceneInteractor by lazy { kosmos.sceneInteractor }
-    private val bouncerInteractor by lazy { kosmos.bouncerInteractor }
-    private val bouncerViewModel by lazy { kosmos.bouncerViewModel }
-    private val underTest by lazy {
-        PatternBouncerViewModel(
-            applicationContext = context,
-            viewModelScope = testScope.backgroundScope,
-            interactor = bouncerInteractor,
+    private val bouncerViewModel by lazy { kosmos.bouncerSceneContentViewModel }
+    private val underTest =
+        kosmos.patternBouncerViewModelFactory.create(
             isInputEnabled = MutableStateFlow(true).asStateFlow(),
             onIntentionalUserInput = {},
         )
-    }
 
     private val containerSize = 90 // px
     private val dotSize = 30 // px
@@ -73,6 +68,7 @@
     fun setUp() {
         overrideResource(R.string.keyguard_enter_your_pattern, ENTER_YOUR_PATTERN)
         overrideResource(R.string.kg_wrong_pattern, WRONG_PATTERN)
+        underTest.activateIn(testScope)
     }
 
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
index 89bafb9..8d82e97 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
@@ -31,10 +31,9 @@
 import com.android.systemui.authentication.domain.interactor.authenticationInteractor
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.bouncer.data.repository.fakeSimBouncerRepository
-import com.android.systemui.bouncer.domain.interactor.bouncerInteractor
-import com.android.systemui.bouncer.domain.interactor.simBouncerInteractor
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.kosmos.testScope
+import com.android.systemui.lifecycle.activateIn
 import com.android.systemui.res.R
 import com.android.systemui.scene.domain.interactor.sceneInteractor
 import com.android.systemui.scene.shared.model.Scenes
@@ -44,7 +43,6 @@
 import kotlin.random.nextInt
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runCurrent
@@ -62,24 +60,18 @@
     private val testScope = kosmos.testScope
     private val sceneInteractor by lazy { kosmos.sceneInteractor }
     private val authenticationInteractor by lazy { kosmos.authenticationInteractor }
-    private val bouncerInteractor by lazy { kosmos.bouncerInteractor }
-    private lateinit var underTest: PinBouncerViewModel
+    private val underTest =
+        kosmos.pinBouncerViewModelFactory.create(
+            isInputEnabled = MutableStateFlow(true),
+            onIntentionalUserInput = {},
+            authenticationMethod = AuthenticationMethodModel.Pin,
+        )
 
     @Before
     fun setUp() {
-        underTest =
-            PinBouncerViewModel(
-                applicationContext = context,
-                viewModelScope = testScope.backgroundScope,
-                interactor = bouncerInteractor,
-                isInputEnabled = MutableStateFlow(true).asStateFlow(),
-                simBouncerInteractor = kosmos.simBouncerInteractor,
-                authenticationMethod = AuthenticationMethodModel.Pin,
-                onIntentionalUserInput = {},
-            )
-
         overrideResource(R.string.keyguard_enter_your_pin, ENTER_YOUR_PIN)
         overrideResource(R.string.kg_wrong_pin, WRONG_PIN)
+        underTest.activateIn(testScope)
     }
 
     @Test
@@ -96,14 +88,10 @@
     fun simBouncerViewModel_simAreaIsVisible() =
         testScope.runTest {
             val underTest =
-                PinBouncerViewModel(
-                    applicationContext = context,
-                    viewModelScope = testScope.backgroundScope,
-                    interactor = bouncerInteractor,
-                    isInputEnabled = MutableStateFlow(true).asStateFlow(),
-                    simBouncerInteractor = kosmos.simBouncerInteractor,
-                    authenticationMethod = AuthenticationMethodModel.Sim,
+                kosmos.pinBouncerViewModelFactory.create(
+                    isInputEnabled = MutableStateFlow(true),
                     onIntentionalUserInput = {},
+                    authenticationMethod = AuthenticationMethodModel.Sim,
                 )
 
             assertThat(underTest.isSimAreaVisible).isTrue()
@@ -125,14 +113,10 @@
     fun simBouncerViewModel_autoConfirmEnabled_hintedPinLengthIsNull() =
         testScope.runTest {
             val underTest =
-                PinBouncerViewModel(
-                    applicationContext = context,
-                    viewModelScope = testScope.backgroundScope,
-                    interactor = bouncerInteractor,
-                    isInputEnabled = MutableStateFlow(true).asStateFlow(),
-                    simBouncerInteractor = kosmos.simBouncerInteractor,
-                    authenticationMethod = AuthenticationMethodModel.Sim,
+                kosmos.pinBouncerViewModelFactory.create(
+                    isInputEnabled = MutableStateFlow(true),
                     onIntentionalUserInput = {},
+                    authenticationMethod = AuthenticationMethodModel.Pin,
                 )
             kosmos.fakeAuthenticationRepository.setAutoConfirmFeatureEnabled(true)
             val hintedPinLength by collectLastValue(underTest.hintedPinLength)
@@ -355,6 +339,7 @@
                 AuthenticationMethodModel.Pin
             )
             kosmos.fakeAuthenticationRepository.setAutoConfirmFeatureEnabled(true)
+            runCurrent()
 
             underTest.onPinButtonClicked(1)
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/WidgetTrampolineInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/WidgetTrampolineInteractorTest.kt
new file mode 100644
index 0000000..b3ffc71
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/WidgetTrampolineInteractorTest.kt
@@ -0,0 +1,220 @@
+/*
+ * 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.communal.domain.interactor
+
+import android.app.ActivityManager.RunningTaskInfo
+import android.app.usage.UsageEvents
+import android.content.pm.UserInfo
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.usagestats.data.repository.fakeUsageStatsRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.plugins.activityStarter
+import com.android.systemui.settings.fakeUserTracker
+import com.android.systemui.shared.system.taskStackChangeListeners
+import com.android.systemui.testKosmos
+import com.android.systemui.util.time.fakeSystemClock
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.milliseconds
+import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceTimeBy
+import kotlinx.coroutines.test.currentTime
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.anyOrNull
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class WidgetTrampolineInteractorTest : SysuiTestCase() {
+
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+
+    private val activityStarter = kosmos.activityStarter
+    private val usageStatsRepository = kosmos.fakeUsageStatsRepository
+    private val taskStackChangeListeners = kosmos.taskStackChangeListeners
+    private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
+    private val userTracker = kosmos.fakeUserTracker
+    private val systemClock = kosmos.fakeSystemClock
+
+    private val underTest = kosmos.widgetTrampolineInteractor
+
+    @Before
+    fun setUp() {
+        userTracker.set(listOf(MAIN_USER), 0)
+        systemClock.setCurrentTimeMillis(testScope.currentTime)
+    }
+
+    @Test
+    fun testNewTaskStartsWhileOnHub_triggersUnlock() =
+        testScope.runTest {
+            transition(from = KeyguardState.LOCKSCREEN, to = KeyguardState.GLANCEABLE_HUB)
+            backgroundScope.launch { underTest.waitForActivityStartAndDismissKeyguard() }
+            runCurrent()
+
+            verify(activityStarter, never()).dismissKeyguardThenExecute(any(), anyOrNull(), any())
+            moveTaskToFront()
+
+            verify(activityStarter).dismissKeyguardThenExecute(any(), anyOrNull(), any())
+        }
+
+    @Test
+    fun testNewTaskStartsAfterExitingHub_doesNotTriggerUnlock() =
+        testScope.runTest {
+            transition(from = KeyguardState.LOCKSCREEN, to = KeyguardState.GLANCEABLE_HUB)
+            backgroundScope.launch { underTest.waitForActivityStartAndDismissKeyguard() }
+            runCurrent()
+
+            transition(from = KeyguardState.GLANCEABLE_HUB, to = KeyguardState.LOCKSCREEN)
+            moveTaskToFront()
+
+            verify(activityStarter, never()).dismissKeyguardThenExecute(any(), anyOrNull(), any())
+        }
+
+    @Test
+    fun testNewTaskStartsAfterTimeout_doesNotTriggerUnlock() =
+        testScope.runTest {
+            transition(from = KeyguardState.LOCKSCREEN, to = KeyguardState.GLANCEABLE_HUB)
+            backgroundScope.launch { underTest.waitForActivityStartAndDismissKeyguard() }
+            runCurrent()
+
+            advanceTime(2.seconds)
+            moveTaskToFront()
+
+            verify(activityStarter, never()).dismissKeyguardThenExecute(any(), anyOrNull(), any())
+        }
+
+    @Test
+    fun testActivityResumedWhileOnHub_triggersUnlock() =
+        testScope.runTest {
+            transition(from = KeyguardState.LOCKSCREEN, to = KeyguardState.GLANCEABLE_HUB)
+            backgroundScope.launch { underTest.waitForActivityStartAndDismissKeyguard() }
+            runCurrent()
+
+            addActivityEvent(UsageEvents.Event.ACTIVITY_RESUMED)
+            advanceTime(1.seconds)
+
+            verify(activityStarter).dismissKeyguardThenExecute(any(), anyOrNull(), any())
+        }
+
+    @Test
+    fun testActivityResumedAfterExitingHub_doesNotTriggerUnlock() =
+        testScope.runTest {
+            transition(from = KeyguardState.LOCKSCREEN, to = KeyguardState.GLANCEABLE_HUB)
+            backgroundScope.launch { underTest.waitForActivityStartAndDismissKeyguard() }
+            runCurrent()
+
+            transition(from = KeyguardState.GLANCEABLE_HUB, to = KeyguardState.LOCKSCREEN)
+            addActivityEvent(UsageEvents.Event.ACTIVITY_RESUMED)
+            advanceTime(1.seconds)
+
+            verify(activityStarter, never()).dismissKeyguardThenExecute(any(), anyOrNull(), any())
+        }
+
+    @Test
+    fun testActivityDestroyed_doesNotTriggerUnlock() =
+        testScope.runTest {
+            transition(from = KeyguardState.LOCKSCREEN, to = KeyguardState.GLANCEABLE_HUB)
+            backgroundScope.launch { underTest.waitForActivityStartAndDismissKeyguard() }
+            runCurrent()
+
+            addActivityEvent(UsageEvents.Event.ACTIVITY_DESTROYED)
+            advanceTime(1.seconds)
+
+            verify(activityStarter, never()).dismissKeyguardThenExecute(any(), anyOrNull(), any())
+        }
+
+    @Test
+    fun testMultipleActivityEvents_triggersUnlockOnlyOnce() =
+        testScope.runTest {
+            transition(from = KeyguardState.LOCKSCREEN, to = KeyguardState.GLANCEABLE_HUB)
+            backgroundScope.launch { underTest.waitForActivityStartAndDismissKeyguard() }
+            runCurrent()
+
+            addActivityEvent(UsageEvents.Event.ACTIVITY_RESUMED)
+            advanceTime(10.milliseconds)
+            addActivityEvent(UsageEvents.Event.ACTIVITY_RESUMED)
+            advanceTime(1.seconds)
+
+            verify(activityStarter, times(1)).dismissKeyguardThenExecute(any(), anyOrNull(), any())
+        }
+
+    private fun TestScope.advanceTime(duration: Duration) {
+        systemClock.advanceTime(duration.inWholeMilliseconds)
+        advanceTimeBy(duration)
+    }
+
+    private fun TestScope.addActivityEvent(type: Int) {
+        usageStatsRepository.addEvent(
+            instanceId = 1,
+            user = MAIN_USER.userHandle,
+            packageName = "pkg.test",
+            timestamp = systemClock.currentTimeMillis(),
+            type = type,
+        )
+        runCurrent()
+    }
+
+    private fun TestScope.moveTaskToFront() {
+        taskStackChangeListeners.listenerImpl.onTaskMovedToFront(mock<RunningTaskInfo>())
+        runCurrent()
+    }
+
+    private suspend fun TestScope.transition(from: KeyguardState, to: KeyguardState) {
+        keyguardTransitionRepository.sendTransitionSteps(
+            listOf(
+                TransitionStep(
+                    from = from,
+                    to = to,
+                    value = 0.1f,
+                    transitionState = TransitionState.STARTED,
+                    ownerName = "test",
+                ),
+                TransitionStep(
+                    from = from,
+                    to = to,
+                    value = 1f,
+                    transitionState = TransitionState.FINISHED,
+                    ownerName = "test",
+                ),
+            ),
+            testScope
+        )
+        runCurrent()
+    }
+
+    private companion object {
+        val MAIN_USER: UserInfo = UserInfo(0, "primary", UserInfo.FLAG_MAIN)
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/WidgetInteractionHandlerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/WidgetInteractionHandlerTest.kt
index 023de52..400f736 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/WidgetInteractionHandlerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/WidgetInteractionHandlerTest.kt
@@ -27,7 +27,9 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.communal.domain.interactor.communalSceneInteractor
+import com.android.systemui.communal.domain.interactor.widgetTrampolineInteractor
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.log.logcatLogBuffer
 import com.android.systemui.plugins.ActivityStarter
@@ -67,9 +69,11 @@
         with(kosmos) {
             underTest =
                 WidgetInteractionHandler(
+                    applicationScope = applicationCoroutineScope,
                     activityStarter = activityStarter,
                     communalSceneInteractor = communalSceneInteractor,
                     logBuffer = logcatLogBuffer(),
+                    widgetTrampolineInteractor = widgetTrampolineInteractor,
                 )
         }
     }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
index 66e45ab..cd84abc 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
@@ -36,10 +36,10 @@
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.bouncer.domain.interactor.BouncerActionButtonInteractor
 import com.android.systemui.bouncer.domain.interactor.bouncerActionButtonInteractor
-import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel
+import com.android.systemui.bouncer.ui.viewmodel.BouncerSceneContentViewModel
 import com.android.systemui.bouncer.ui.viewmodel.PasswordBouncerViewModel
 import com.android.systemui.bouncer.ui.viewmodel.PinBouncerViewModel
-import com.android.systemui.bouncer.ui.viewmodel.bouncerViewModel
+import com.android.systemui.bouncer.ui.viewmodel.bouncerSceneContentViewModel
 import com.android.systemui.classifier.domain.interactor.falsingInteractor
 import com.android.systemui.communal.domain.interactor.communalInteractor
 import com.android.systemui.coroutines.collectLastValue
@@ -139,7 +139,7 @@
 
     private lateinit var mobileConnectionsRepository: FakeMobileConnectionsRepository
     private lateinit var bouncerActionButtonInteractor: BouncerActionButtonInteractor
-    private lateinit var bouncerViewModel: BouncerViewModel
+    private lateinit var bouncerSceneContentViewModel: BouncerSceneContentViewModel
 
     private val lockscreenSceneActionsViewModel by lazy {
         LockscreenSceneActionsViewModel(
@@ -187,7 +187,7 @@
         }
 
         bouncerActionButtonInteractor = kosmos.bouncerActionButtonInteractor
-        bouncerViewModel = kosmos.bouncerViewModel
+        bouncerSceneContentViewModel = kosmos.bouncerSceneContentViewModel
 
         shadeSceneContentViewModel = kosmos.shadeSceneContentViewModel
         shadeSceneActionsViewModel = kosmos.shadeSceneActionsViewModel
@@ -198,6 +198,7 @@
         lockscreenSceneActionsViewModel.activateIn(testScope)
         shadeSceneContentViewModel.activateIn(testScope)
         shadeSceneActionsViewModel.activateIn(testScope)
+        bouncerSceneContentViewModel.activateIn(testScope)
 
         assertWithMessage("Initial scene key mismatch!")
             .that(sceneContainerViewModel.currentScene.value)
@@ -397,7 +398,7 @@
             assertThat(upDestinationSceneKey).isEqualTo(Scenes.Bouncer)
             emulateUserDrivenTransition(to = upDestinationSceneKey)
 
-            val bouncerActionButton by collectLastValue(bouncerViewModel.actionButton)
+            val bouncerActionButton by collectLastValue(bouncerSceneContentViewModel.actionButton)
             assertWithMessage("Bouncer action button not visible")
                 .that(bouncerActionButton)
                 .isNotNull()
@@ -417,7 +418,7 @@
             assertThat(upDestinationSceneKey).isEqualTo(Scenes.Bouncer)
             emulateUserDrivenTransition(to = upDestinationSceneKey)
 
-            val bouncerActionButton by collectLastValue(bouncerViewModel.actionButton)
+            val bouncerActionButton by collectLastValue(bouncerSceneContentViewModel.actionButton)
             assertWithMessage("Bouncer action button not visible during call")
                 .that(bouncerActionButton)
                 .isNotNull()
@@ -568,7 +569,7 @@
         bouncerSceneJob =
             if (to == Scenes.Bouncer) {
                 testScope.backgroundScope.launch {
-                    bouncerViewModel.authMethodViewModel.collect {
+                    bouncerSceneContentViewModel.authMethodViewModel.collect {
                         // Do nothing. Need this to turn this otherwise cold flow, hot.
                     }
                 }
@@ -644,7 +645,8 @@
         assertWithMessage("Cannot enter PIN when not on the Bouncer scene!")
             .that(getCurrentSceneInUi())
             .isEqualTo(Scenes.Bouncer)
-        val authMethodViewModel by collectLastValue(bouncerViewModel.authMethodViewModel)
+        val authMethodViewModel by
+            collectLastValue(bouncerSceneContentViewModel.authMethodViewModel)
         assertWithMessage("Cannot enter PIN when not using a PIN authentication method!")
             .that(authMethodViewModel)
             .isInstanceOf(PinBouncerViewModel::class.java)
@@ -672,7 +674,8 @@
         assertWithMessage("Cannot enter PIN when not on the Bouncer scene!")
             .that(getCurrentSceneInUi())
             .isEqualTo(Scenes.Bouncer)
-        val authMethodViewModel by collectLastValue(bouncerViewModel.authMethodViewModel)
+        val authMethodViewModel by
+            collectLastValue(bouncerSceneContentViewModel.authMethodViewModel)
         assertWithMessage("Cannot enter PIN when not using a PIN authentication method!")
             .that(authMethodViewModel)
             .isInstanceOf(PinBouncerViewModel::class.java)
@@ -719,7 +722,7 @@
 
     /** Emulates the dismissal of the IME (soft keyboard). */
     private fun TestScope.dismissIme() {
-        (bouncerViewModel.authMethodViewModel.value as? PasswordBouncerViewModel)?.let {
+        (bouncerSceneContentViewModel.authMethodViewModel.value as? PasswordBouncerViewModel)?.let {
             it.onImeDismissed()
             runCurrent()
         }
diff --git a/packages/SystemUI/src/com/android/systemui/SwipeHelper.java b/packages/SystemUI/src/com/android/systemui/SwipeHelper.java
index 63ad41a..13cd2c5 100644
--- a/packages/SystemUI/src/com/android/systemui/SwipeHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/SwipeHelper.java
@@ -53,6 +53,7 @@
 import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
 import com.android.systemui.res.R;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
+import com.android.systemui.statusbar.notification.shared.NotificationContentAlphaOptimization;
 import com.android.wm.shell.animation.FlingAnimationUtils;
 import com.android.wm.shell.shared.animation.PhysicsAnimator;
 import com.android.wm.shell.shared.animation.PhysicsAnimator.SpringConfig;
@@ -63,13 +64,6 @@
 public class SwipeHelper implements Gefingerpoken, Dumpable {
     static final String TAG = "com.android.systemui.SwipeHelper";
     private static final boolean DEBUG_INVALIDATE = false;
-    private static final boolean CONSTRAIN_SWIPE = true;
-    private static final boolean FADE_OUT_DURING_SWIPE = true;
-    private static final boolean DISMISS_IF_SWIPED_FAR_ENOUGH = true;
-
-    public static final int X = 0;
-    public static final int Y = 1;
-
     private static final float SWIPE_ESCAPE_VELOCITY = 500f; // dp/sec
     private static final int DEFAULT_ESCAPE_ANIMATION_DURATION = 200; // ms
     private static final int MAX_ESCAPE_ANIMATION_DURATION = 400; // ms
@@ -171,10 +165,6 @@
         mPagingTouchSlop = pagingTouchSlop;
     }
 
-    public void setDisableHardwareLayers(boolean disableHwLayers) {
-        mDisableHwLayers = disableHwLayers;
-    }
-
     private float getPos(MotionEvent ev) {
         return ev.getX();
     }
@@ -253,13 +243,14 @@
             float translation) {
         float swipeProgress = getSwipeProgressForOffset(animView, translation);
         if (!mCallback.updateSwipeProgress(animView, dismissable, swipeProgress)) {
-            if (FADE_OUT_DURING_SWIPE && dismissable) {
-                if (!mDisableHwLayers) {
-                    if (swipeProgress != 0f && swipeProgress != 1f) {
-                        animView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
-                    } else {
-                        animView.setLayerType(View.LAYER_TYPE_NONE, null);
-                    }
+            if (dismissable
+                    || (NotificationContentAlphaOptimization.isEnabled() && translation == 0)) {
+                // We need to reset the content alpha even when the view is not dismissible (eg.
+                //  when Guts is visible)
+                if (swipeProgress != 0f && swipeProgress != 1f) {
+                    animView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
+                } else {
+                    animView.setLayerType(View.LAYER_TYPE_NONE, null);
                 }
                 updateSwipeProgressAlpha(animView, getSwipeAlpha(swipeProgress));
             }
@@ -436,9 +427,7 @@
             duration = fixedDuration;
         }
 
-        if (!mDisableHwLayers) {
-            animView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
-        }
+        animView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
         AnimatorUpdateListener updateListener = new AnimatorUpdateListener() {
             @Override
             public void onAnimationUpdate(ValueAnimator animation) {
@@ -493,9 +482,7 @@
                 if (endAction != null) {
                     endAction.accept(mCancelled);
                 }
-                if (!mDisableHwLayers) {
-                    animView.setLayerType(View.LAYER_TYPE_NONE, null);
-                }
+                animView.setLayerType(View.LAYER_TYPE_NONE, null);
                 onDismissChildWithAnimationFinished();
             }
         });
@@ -612,7 +599,11 @@
      * view is being animated to dismiss or snap.
      */
     public void onTranslationUpdate(View animView, float value, boolean canBeDismissed) {
-        updateSwipeProgressFromOffset(animView, canBeDismissed, value);
+        updateSwipeProgressFromOffset(
+                animView,
+                /* dismissable= */ canBeDismissed,
+                /* translation= */ value
+        );
     }
 
     private void snapChildInstantly(final View view) {
@@ -689,7 +680,7 @@
                     } else {
                         // don't let items that can't be dismissed be dragged more than
                         // maxScrollDistance
-                        if (CONSTRAIN_SWIPE && !mCallback.canChildBeDismissedInDirection(
+                        if (!mCallback.canChildBeDismissedInDirection(
                                 mTouchedView,
                                 delta > 0)) {
                             float size = getSize(mTouchedView);
@@ -761,8 +752,7 @@
 
     protected boolean swipedFarEnough() {
         float translation = getTranslation(mTouchedView);
-        return DISMISS_IF_SWIPED_FAR_ENOUGH
-                && Math.abs(translation) > SWIPED_FAR_ENOUGH_SIZE_FRACTION * getSize(
+        return Math.abs(translation) > SWIPED_FAR_ENOUGH_SIZE_FRACTION * getSize(
                 mTouchedView);
     }
 
@@ -822,9 +812,18 @@
     }
 
     public void forceResetSwipeState(@NonNull View view) {
-        if (view.getTranslationX() == 0) return;
+        if (view.getTranslationX() == 0
+                && (!NotificationContentAlphaOptimization.isEnabled() || view.getAlpha() == 1f)
+        ) {
+            // Don't do anything when translation is 0 and alpha is 1
+            return;
+        }
         setTranslation(view, 0);
-        updateSwipeProgressFromOffset(view, /* dismissable= */ true, 0);
+        updateSwipeProgressFromOffset(
+                view,
+                /* dismissable= */ true,
+                /* translation= */ 0
+        );
     }
 
     /** This method resets the swipe state, and if `resetAll` is true, also resets the snap state */
@@ -893,7 +892,6 @@
         pw.append("mTranslation=").println(mTranslation);
         pw.append("mCanCurrViewBeDimissed=").println(mCanCurrViewBeDimissed);
         pw.append("mMenuRowIntercepting=").println(mMenuRowIntercepting);
-        pw.append("mDisableHwLayers=").println(mDisableHwLayers);
         pw.append("mDismissPendingMap: ").println(mDismissPendingMap.size());
         if (!mDismissPendingMap.isEmpty()) {
             mDismissPendingMap.forEach((view, animator) -> {
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerViewModule.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerViewModule.kt
index aebc50f..3410782 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerViewModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerViewModule.kt
@@ -18,8 +18,6 @@
 
 import android.app.AlertDialog
 import android.content.Context
-import com.android.systemui.bouncer.ui.viewmodel.BouncerMessageViewModelModule
-import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModelModule
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.statusbar.phone.SystemUIDialog
@@ -27,13 +25,7 @@
 import dagger.Module
 import dagger.Provides
 
-@Module(
-    includes =
-        [
-            BouncerViewModelModule::class,
-            BouncerMessageViewModelModule::class,
-        ],
-)
+@Module
 interface BouncerViewModule {
     /** Binds BouncerView to BouncerViewImpl and makes it injectable. */
     @Binds fun bindBouncerView(bouncerViewImpl: BouncerViewImpl): BouncerView
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/BouncerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/BouncerViewBinder.kt
index 78811a9..ad93a25 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/BouncerViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/BouncerViewBinder.kt
@@ -9,7 +9,7 @@
 import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
 import com.android.systemui.bouncer.shared.flag.ComposeBouncerFlags
 import com.android.systemui.bouncer.ui.BouncerDialogFactory
-import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel
+import com.android.systemui.bouncer.ui.viewmodel.BouncerSceneContentViewModel
 import com.android.systemui.bouncer.ui.viewmodel.KeyguardBouncerViewModel
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel
@@ -40,7 +40,7 @@
 @Inject
 constructor(
     val legacyInteractor: PrimaryBouncerInteractor,
-    val viewModel: BouncerViewModel,
+    val viewModelFactory: BouncerSceneContentViewModel.Factory,
     val dialogFactory: BouncerDialogFactory,
     val authenticationInteractor: AuthenticationInteractor,
     val viewMediatorCallback: ViewMediatorCallback?,
@@ -65,7 +65,7 @@
             ComposeBouncerViewBinder.bind(
                 view,
                 deps.legacyInteractor,
-                deps.viewModel,
+                deps.viewModelFactory,
                 deps.dialogFactory,
                 deps.authenticationInteractor,
                 deps.selectedUserInteractor,
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/ComposeBouncerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/ComposeBouncerViewBinder.kt
index eaca276..c1f7d59 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/ComposeBouncerViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/ComposeBouncerViewBinder.kt
@@ -14,7 +14,8 @@
 import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
 import com.android.systemui.bouncer.ui.BouncerDialogFactory
 import com.android.systemui.bouncer.ui.composable.BouncerContent
-import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel
+import com.android.systemui.bouncer.ui.viewmodel.BouncerSceneContentViewModel
+import com.android.systemui.lifecycle.rememberViewModel
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.user.domain.interactor.SelectedUserInteractor
 import kotlinx.coroutines.flow.collectLatest
@@ -25,7 +26,7 @@
     fun bind(
         view: ViewGroup,
         legacyInteractor: PrimaryBouncerInteractor,
-        viewModel: BouncerViewModel,
+        viewModelFactory: BouncerSceneContentViewModel.Factory,
         dialogFactory: BouncerDialogFactory,
         authenticationInteractor: AuthenticationInteractor,
         selectedUserInteractor: SelectedUserInteractor,
@@ -48,7 +49,14 @@
                                     this@repeatWhenAttached.lifecycle
                             }
                         )
-                        setContent { PlatformTheme { BouncerContent(viewModel, dialogFactory) } }
+                        setContent {
+                            PlatformTheme {
+                                BouncerContent(
+                                    rememberViewModel { viewModelFactory.create() },
+                                    dialogFactory,
+                                )
+                            }
+                        }
                     }
                 }
             }
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt
index 4fbf735..e7dd974 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt
@@ -17,17 +17,18 @@
 package com.android.systemui.bouncer.ui.viewmodel
 
 import android.annotation.StringRes
+import com.android.app.tracing.coroutines.flow.collectLatest
 import com.android.systemui.authentication.domain.interactor.AuthenticationResult
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
-import kotlinx.coroutines.CoroutineScope
+import com.android.systemui.lifecycle.SysUiViewModel
+import kotlinx.coroutines.channels.Channel
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.launch
+import kotlinx.coroutines.flow.receiveAsFlow
 
 sealed class AuthMethodBouncerViewModel(
-    protected val viewModelScope: CoroutineScope,
     protected val interactor: BouncerInteractor,
 
     /**
@@ -37,7 +38,7 @@
      * being able to attempt to unlock the device.
      */
     val isInputEnabled: StateFlow<Boolean>,
-) {
+) : SysUiViewModel() {
 
     private val _animateFailure = MutableStateFlow(false)
     /**
@@ -57,6 +58,29 @@
      */
     @get:StringRes abstract val lockoutMessageId: Int
 
+    private val authenticationRequests = Channel<AuthenticationRequest>(Channel.BUFFERED)
+
+    override suspend fun onActivated() {
+        authenticationRequests.receiveAsFlow().collectLatest { request ->
+            if (!isInputEnabled.value) {
+                return@collectLatest
+            }
+
+            val authenticationResult =
+                interactor.authenticate(
+                    input = request.input,
+                    tryAutoConfirm = request.useAutoConfirm,
+                )
+
+            if (authenticationResult == AuthenticationResult.SKIPPED && request.useAutoConfirm) {
+                return@collectLatest
+            }
+
+            _animateFailure.value = authenticationResult != AuthenticationResult.SUCCEEDED
+            clearInput()
+        }
+    }
+
     /**
      * Notifies that the UI has been hidden from the user (after any transitions have completed).
      */
@@ -92,14 +116,11 @@
         input: List<Any> = getInput(),
         useAutoConfirm: Boolean = false,
     ) {
-        viewModelScope.launch {
-            val authenticationResult = interactor.authenticate(input, useAutoConfirm)
-            if (authenticationResult == AuthenticationResult.SKIPPED && useAutoConfirm) {
-                return@launch
-            }
-            _animateFailure.value = authenticationResult != AuthenticationResult.SUCCEEDED
-
-            clearInput()
-        }
+        authenticationRequests.trySend(AuthenticationRequest(input, useAutoConfirm))
     }
+
+    private data class AuthenticationRequest(
+        val input: List<Any>,
+        val useAutoConfirm: Boolean,
+    )
 }
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModel.kt
index 31479f1..c3215b4 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModel.kt
@@ -27,7 +27,6 @@
 import com.android.systemui.bouncer.shared.model.BouncerMessageStrings
 import com.android.systemui.bouncer.shared.model.primaryMessage
 import com.android.systemui.bouncer.shared.model.secondaryMessage
-import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.deviceentry.domain.interactor.BiometricMessageInteractor
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryBiometricsAllowedInteractor
@@ -39,19 +38,19 @@
 import com.android.systemui.deviceentry.shared.model.FaceTimeoutMessage
 import com.android.systemui.deviceentry.shared.model.FingerprintFailureMessage
 import com.android.systemui.deviceentry.shared.model.FingerprintLockoutMessage
+import com.android.systemui.lifecycle.SysUiViewModel
 import com.android.systemui.res.R.string.kg_too_many_failed_attempts_countdown
 import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel
-import com.android.systemui.user.ui.viewmodel.UserViewModel
 import com.android.systemui.util.kotlin.Utils.Companion.sample
 import com.android.systemui.util.time.SystemClock
-import dagger.Module
-import dagger.Provides
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
 import kotlin.math.ceil
 import kotlin.math.max
 import kotlin.time.Duration.Companion.seconds
-import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.Job
+import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableSharedFlow
@@ -65,20 +64,21 @@
 
 /** Holds UI state for the 2-line status message shown on the bouncer. */
 @OptIn(ExperimentalCoroutinesApi::class)
-class BouncerMessageViewModel(
+class BouncerMessageViewModel
+@AssistedInject
+constructor(
     @Application private val applicationContext: Context,
-    @Application private val applicationScope: CoroutineScope,
     private val bouncerInteractor: BouncerInteractor,
     private val simBouncerInteractor: SimBouncerInteractor,
     private val authenticationInteractor: AuthenticationInteractor,
-    selectedUser: Flow<UserViewModel>,
+    private val userSwitcherViewModel: UserSwitcherViewModel,
     private val clock: SystemClock,
     private val biometricMessageInteractor: BiometricMessageInteractor,
     private val faceAuthInteractor: DeviceEntryFaceAuthInteractor,
     private val deviceUnlockedInteractor: DeviceUnlockedInteractor,
     private val deviceEntryBiometricsAllowedInteractor: DeviceEntryBiometricsAllowedInteractor,
-    flags: ComposeBouncerFlags,
-) {
+    private val flags: ComposeBouncerFlags,
+) : SysUiViewModel() {
     /**
      * A message shown when the user has attempted the wrong credential too many times and now must
      * wait a while before attempting to authenticate again.
@@ -94,6 +94,25 @@
     /** The user-facing message to show in the bouncer. */
     val message: MutableStateFlow<MessageViewModel?> = MutableStateFlow(null)
 
+    override suspend fun onActivated() {
+        if (!flags.isComposeBouncerOrSceneContainerEnabled()) {
+            return
+        }
+
+        coroutineScope {
+            launch {
+                // Update the lockout countdown whenever the selected user is switched.
+                userSwitcherViewModel.selectedUser.collect { startLockoutCountdown() }
+            }
+
+            launch { defaultBouncerMessageInitializer() }
+            launch { listenForSimBouncerEvents() }
+            launch { listenForBouncerEvents() }
+            launch { listenForFaceMessages() }
+            launch { listenForFingerprintMessages() }
+        }
+    }
+
     /** Initializes the bouncer message to default whenever it is shown. */
     fun onShown() {
         showDefaultMessage()
@@ -108,173 +127,161 @@
 
     private var lockoutCountdownJob: Job? = null
 
-    private fun defaultBouncerMessageInitializer() {
-        applicationScope.launch {
-            resetToDefault.emit(Unit)
-            authenticationInteractor.authenticationMethod
-                .flatMapLatest { authMethod ->
-                    if (authMethod == AuthenticationMethodModel.Sim) {
-                        resetToDefault.map {
-                            MessageViewModel(simBouncerInteractor.getDefaultMessage())
-                        }
-                    } else if (authMethod.isSecure) {
-                        combine(
-                            deviceUnlockedInteractor.deviceEntryRestrictionReason,
-                            lockoutMessage,
-                            deviceEntryBiometricsAllowedInteractor
-                                .isFingerprintCurrentlyAllowedOnBouncer,
-                            resetToDefault,
-                        ) { deviceEntryRestrictedReason, lockoutMsg, isFpAllowedInBouncer, _ ->
-                            lockoutMsg
-                                ?: deviceEntryRestrictedReason.toMessage(
-                                    authMethod,
-                                    isFpAllowedInBouncer
-                                )
-                        }
-                    } else {
-                        emptyFlow()
+    private suspend fun defaultBouncerMessageInitializer() {
+        resetToDefault.emit(Unit)
+        authenticationInteractor.authenticationMethod
+            .flatMapLatest { authMethod ->
+                if (authMethod == AuthenticationMethodModel.Sim) {
+                    resetToDefault.map {
+                        MessageViewModel(simBouncerInteractor.getDefaultMessage())
                     }
+                } else if (authMethod.isSecure) {
+                    combine(
+                        deviceUnlockedInteractor.deviceEntryRestrictionReason,
+                        lockoutMessage,
+                        deviceEntryBiometricsAllowedInteractor
+                            .isFingerprintCurrentlyAllowedOnBouncer,
+                        resetToDefault,
+                    ) { deviceEntryRestrictedReason, lockoutMsg, isFpAllowedInBouncer, _ ->
+                        lockoutMsg
+                            ?: deviceEntryRestrictedReason.toMessage(
+                                authMethod,
+                                isFpAllowedInBouncer
+                            )
+                    }
+                } else {
+                    emptyFlow()
                 }
-                .collectLatest { messageViewModel -> message.value = messageViewModel }
-        }
+            }
+            .collectLatest { messageViewModel -> message.value = messageViewModel }
     }
 
-    private fun listenForSimBouncerEvents() {
+    private suspend fun listenForSimBouncerEvents() {
         // Listen for any events from the SIM bouncer and update the message shown on the bouncer.
-        applicationScope.launch {
-            authenticationInteractor.authenticationMethod
-                .flatMapLatest { authMethod ->
-                    if (authMethod == AuthenticationMethodModel.Sim) {
-                        simBouncerInteractor.bouncerMessageChanged.map { simMsg ->
-                            simMsg?.let { MessageViewModel(it) }
-                        }
-                    } else {
-                        emptyFlow()
+        authenticationInteractor.authenticationMethod
+            .flatMapLatest { authMethod ->
+                if (authMethod == AuthenticationMethodModel.Sim) {
+                    simBouncerInteractor.bouncerMessageChanged.map { simMsg ->
+                        simMsg?.let { MessageViewModel(it) }
                     }
+                } else {
+                    emptyFlow()
                 }
-                .collectLatest {
-                    if (it != null) {
-                        message.value = it
-                    } else {
-                        resetToDefault.emit(Unit)
-                    }
+            }
+            .collectLatest {
+                if (it != null) {
+                    message.value = it
+                } else {
+                    resetToDefault.emit(Unit)
                 }
-        }
+            }
     }
 
-    private fun listenForFaceMessages() {
+    private suspend fun listenForFaceMessages() {
         // Listen for any events from face authentication and update the message shown on the
         // bouncer.
-        applicationScope.launch {
-            biometricMessageInteractor.faceMessage
-                .sample(
-                    authenticationInteractor.authenticationMethod,
-                    deviceEntryBiometricsAllowedInteractor.isFingerprintCurrentlyAllowedOnBouncer,
-                )
-                .collectLatest { (faceMessage, authMethod, fingerprintAllowedOnBouncer) ->
-                    val isFaceAuthStrong = faceAuthInteractor.isFaceAuthStrong()
-                    val defaultPrimaryMessage =
-                        BouncerMessageStrings.defaultMessage(
-                                authMethod,
-                                fingerprintAllowedOnBouncer
+        biometricMessageInteractor.faceMessage
+            .sample(
+                authenticationInteractor.authenticationMethod,
+                deviceEntryBiometricsAllowedInteractor.isFingerprintCurrentlyAllowedOnBouncer,
+            )
+            .collectLatest { (faceMessage, authMethod, fingerprintAllowedOnBouncer) ->
+                val isFaceAuthStrong = faceAuthInteractor.isFaceAuthStrong()
+                val defaultPrimaryMessage =
+                    BouncerMessageStrings.defaultMessage(authMethod, fingerprintAllowedOnBouncer)
+                        .primaryMessage
+                        .toResString()
+                message.value =
+                    when (faceMessage) {
+                        is FaceTimeoutMessage ->
+                            MessageViewModel(
+                                text = defaultPrimaryMessage,
+                                secondaryText = faceMessage.message,
+                                isUpdateAnimated = true
                             )
-                            .primaryMessage
-                            .toResString()
-                    message.value =
-                        when (faceMessage) {
-                            is FaceTimeoutMessage ->
-                                MessageViewModel(
-                                    text = defaultPrimaryMessage,
-                                    secondaryText = faceMessage.message,
-                                    isUpdateAnimated = true
-                                )
-                            is FaceLockoutMessage ->
-                                if (isFaceAuthStrong)
-                                    BouncerMessageStrings.class3AuthLockedOut(authMethod)
-                                        .toMessage()
-                                else
-                                    BouncerMessageStrings.faceLockedOut(
-                                            authMethod,
-                                            fingerprintAllowedOnBouncer
-                                        )
-                                        .toMessage()
-                            is FaceFailureMessage ->
-                                BouncerMessageStrings.incorrectFaceInput(
+                        is FaceLockoutMessage ->
+                            if (isFaceAuthStrong)
+                                BouncerMessageStrings.class3AuthLockedOut(authMethod).toMessage()
+                            else
+                                BouncerMessageStrings.faceLockedOut(
                                         authMethod,
                                         fingerprintAllowedOnBouncer
                                     )
                                     .toMessage()
-                            else ->
-                                MessageViewModel(
-                                    text = defaultPrimaryMessage,
-                                    secondaryText = faceMessage.message,
-                                    isUpdateAnimated = false
+                        is FaceFailureMessage ->
+                            BouncerMessageStrings.incorrectFaceInput(
+                                    authMethod,
+                                    fingerprintAllowedOnBouncer
                                 )
-                        }
-                    delay(MESSAGE_DURATION)
-                    resetToDefault.emit(Unit)
-                }
-        }
-    }
-
-    private fun listenForFingerprintMessages() {
-        applicationScope.launch {
-            // Listen for any events from fingerprint authentication and update the message shown
-            // on the bouncer.
-            biometricMessageInteractor.fingerprintMessage
-                .sample(
-                    authenticationInteractor.authenticationMethod,
-                    deviceEntryBiometricsAllowedInteractor.isFingerprintCurrentlyAllowedOnBouncer
-                )
-                .collectLatest { (fingerprintMessage, authMethod, isFingerprintAllowed) ->
-                    val defaultPrimaryMessage =
-                        BouncerMessageStrings.defaultMessage(authMethod, isFingerprintAllowed)
-                            .primaryMessage
-                            .toResString()
-                    message.value =
-                        when (fingerprintMessage) {
-                            is FingerprintLockoutMessage ->
-                                BouncerMessageStrings.class3AuthLockedOut(authMethod).toMessage()
-                            is FingerprintFailureMessage ->
-                                BouncerMessageStrings.incorrectFingerprintInput(authMethod)
-                                    .toMessage()
-                            else ->
-                                MessageViewModel(
-                                    text = defaultPrimaryMessage,
-                                    secondaryText = fingerprintMessage.message,
-                                    isUpdateAnimated = false
-                                )
-                        }
-                    delay(MESSAGE_DURATION)
-                    resetToDefault.emit(Unit)
-                }
-        }
-    }
-
-    private fun listenForBouncerEvents() {
-        // Keeps the lockout message up-to-date.
-        applicationScope.launch {
-            bouncerInteractor.onLockoutStarted.collect { startLockoutCountdown() }
-        }
-
-        // Listens to relevant bouncer events
-        applicationScope.launch {
-            bouncerInteractor.onIncorrectBouncerInput
-                .sample(
-                    authenticationInteractor.authenticationMethod,
-                    deviceEntryBiometricsAllowedInteractor.isFingerprintCurrentlyAllowedOnBouncer
-                )
-                .collectLatest { (_, authMethod, isFingerprintAllowed) ->
-                    message.emit(
-                        BouncerMessageStrings.incorrectSecurityInput(
-                                authMethod,
-                                isFingerprintAllowed
+                                .toMessage()
+                        else ->
+                            MessageViewModel(
+                                text = defaultPrimaryMessage,
+                                secondaryText = faceMessage.message,
+                                isUpdateAnimated = false
                             )
-                            .toMessage()
+                    }
+                delay(MESSAGE_DURATION)
+                resetToDefault.emit(Unit)
+            }
+    }
+
+    private suspend fun listenForFingerprintMessages() {
+        // Listen for any events from fingerprint authentication and update the message shown
+        // on the bouncer.
+        biometricMessageInteractor.fingerprintMessage
+            .sample(
+                authenticationInteractor.authenticationMethod,
+                deviceEntryBiometricsAllowedInteractor.isFingerprintCurrentlyAllowedOnBouncer
+            )
+            .collectLatest { (fingerprintMessage, authMethod, isFingerprintAllowed) ->
+                val defaultPrimaryMessage =
+                    BouncerMessageStrings.defaultMessage(authMethod, isFingerprintAllowed)
+                        .primaryMessage
+                        .toResString()
+                message.value =
+                    when (fingerprintMessage) {
+                        is FingerprintLockoutMessage ->
+                            BouncerMessageStrings.class3AuthLockedOut(authMethod).toMessage()
+                        is FingerprintFailureMessage ->
+                            BouncerMessageStrings.incorrectFingerprintInput(authMethod).toMessage()
+                        else ->
+                            MessageViewModel(
+                                text = defaultPrimaryMessage,
+                                secondaryText = fingerprintMessage.message,
+                                isUpdateAnimated = false
+                            )
+                    }
+                delay(MESSAGE_DURATION)
+                resetToDefault.emit(Unit)
+            }
+    }
+
+    private suspend fun listenForBouncerEvents() {
+        coroutineScope {
+            // Keeps the lockout message up-to-date.
+            launch { bouncerInteractor.onLockoutStarted.collect { startLockoutCountdown() } }
+
+            // Listens to relevant bouncer events
+            launch {
+                bouncerInteractor.onIncorrectBouncerInput
+                    .sample(
+                        authenticationInteractor.authenticationMethod,
+                        deviceEntryBiometricsAllowedInteractor
+                            .isFingerprintCurrentlyAllowedOnBouncer
                     )
-                    delay(MESSAGE_DURATION)
-                    resetToDefault.emit(Unit)
-                }
+                    .collectLatest { (_, authMethod, isFingerprintAllowed) ->
+                        message.emit(
+                            BouncerMessageStrings.incorrectSecurityInput(
+                                    authMethod,
+                                    isFingerprintAllowed
+                                )
+                                .toMessage()
+                        )
+                        delay(MESSAGE_DURATION)
+                        resetToDefault.emit(Unit)
+                    }
+            }
         }
     }
 
@@ -323,10 +330,10 @@
     }
 
     /** Shows the countdown message and refreshes it every second. */
-    private fun startLockoutCountdown() {
+    private suspend fun startLockoutCountdown() {
         lockoutCountdownJob?.cancel()
-        lockoutCountdownJob =
-            applicationScope.launch {
+        lockoutCountdownJob = coroutineScope {
+            launch {
                 authenticationInteractor.authenticationMethod.collectLatest { authMethod ->
                     do {
                         val remainingSeconds = remainingLockoutSeconds()
@@ -352,6 +359,7 @@
                     lockoutCountdownJob = null
                 }
             }
+        }
     }
 
     private fun remainingLockoutSeconds(): Int {
@@ -365,20 +373,9 @@
 
     private fun Int.toResString(): String = applicationContext.getString(this)
 
-    init {
-        if (flags.isComposeBouncerOrSceneContainerEnabled()) {
-            applicationScope.launch {
-                // Update the lockout countdown whenever the selected user is switched.
-                selectedUser.collect { startLockoutCountdown() }
-            }
-
-            defaultBouncerMessageInitializer()
-
-            listenForSimBouncerEvents()
-            listenForBouncerEvents()
-            listenForFaceMessages()
-            listenForFingerprintMessages()
-        }
+    @AssistedFactory
+    interface Factory {
+        fun create(): BouncerMessageViewModel
     }
 
     companion object {
@@ -398,40 +395,3 @@
      */
     val isUpdateAnimated: Boolean = true,
 )
-
-@OptIn(ExperimentalCoroutinesApi::class)
-@Module
-object BouncerMessageViewModelModule {
-
-    @Provides
-    @SysUISingleton
-    fun viewModel(
-        @Application applicationContext: Context,
-        @Application applicationScope: CoroutineScope,
-        bouncerInteractor: BouncerInteractor,
-        simBouncerInteractor: SimBouncerInteractor,
-        authenticationInteractor: AuthenticationInteractor,
-        clock: SystemClock,
-        biometricMessageInteractor: BiometricMessageInteractor,
-        faceAuthInteractor: DeviceEntryFaceAuthInteractor,
-        deviceUnlockedInteractor: DeviceUnlockedInteractor,
-        deviceEntryBiometricsAllowedInteractor: DeviceEntryBiometricsAllowedInteractor,
-        flags: ComposeBouncerFlags,
-        userSwitcherViewModel: UserSwitcherViewModel,
-    ): BouncerMessageViewModel {
-        return BouncerMessageViewModel(
-            applicationContext = applicationContext,
-            applicationScope = applicationScope,
-            bouncerInteractor = bouncerInteractor,
-            simBouncerInteractor = simBouncerInteractor,
-            authenticationInteractor = authenticationInteractor,
-            clock = clock,
-            biometricMessageInteractor = biometricMessageInteractor,
-            faceAuthInteractor = faceAuthInteractor,
-            deviceUnlockedInteractor = deviceUnlockedInteractor,
-            deviceEntryBiometricsAllowedInteractor = deviceEntryBiometricsAllowedInteractor,
-            flags = flags,
-            selectedUser = userSwitcherViewModel.selectedUser,
-        )
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneActionsViewModel.kt
new file mode 100644
index 0000000..2a27271
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneActionsViewModel.kt
@@ -0,0 +1,56 @@
+/*
+ * 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.bouncer.ui.viewmodel
+
+import com.android.compose.animation.scene.Back
+import com.android.compose.animation.scene.Swipe
+import com.android.compose.animation.scene.SwipeDirection
+import com.android.compose.animation.scene.UserAction
+import com.android.compose.animation.scene.UserActionResult
+import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
+import com.android.systemui.scene.ui.viewmodel.SceneActionsViewModel
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.map
+
+/**
+ * Models UI state for user actions that can lead to navigation to other scenes when showing the
+ * bouncer scene.
+ */
+class BouncerSceneActionsViewModel
+@AssistedInject
+constructor(
+    private val bouncerInteractor: BouncerInteractor,
+) : SceneActionsViewModel() {
+
+    override suspend fun hydrateActions(setActions: (Map<UserAction, UserActionResult>) -> Unit) {
+        bouncerInteractor.dismissDestination
+            .map { prevScene ->
+                mapOf(
+                    Back to UserActionResult(prevScene),
+                    Swipe(SwipeDirection.Down) to UserActionResult(prevScene),
+                )
+            }
+            .collectLatest { actions -> setActions(actions) }
+    }
+
+    @AssistedFactory
+    interface Factory {
+        fun create(): BouncerSceneActionsViewModel
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneContentViewModel.kt
new file mode 100644
index 0000000..aede63b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneContentViewModel.kt
@@ -0,0 +1,362 @@
+/*
+ * 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.bouncer.ui.viewmodel
+
+import android.app.admin.DevicePolicyManager
+import android.app.admin.DevicePolicyResources
+import android.content.Context
+import android.graphics.Bitmap
+import androidx.compose.ui.input.key.KeyEvent
+import androidx.compose.ui.input.key.type
+import androidx.core.graphics.drawable.toBitmap
+import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
+import com.android.systemui.authentication.shared.model.AuthenticationWipeModel
+import com.android.systemui.bouncer.domain.interactor.BouncerActionButtonInteractor
+import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
+import com.android.systemui.bouncer.shared.flag.ComposeBouncerFlags
+import com.android.systemui.bouncer.shared.model.BouncerActionButtonModel
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.common.shared.model.Text
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.lifecycle.SysUiViewModel
+import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
+
+/** Models UI state for the content of the bouncer scene. */
+class BouncerSceneContentViewModel
+@AssistedInject
+constructor(
+    @Application private val applicationContext: Context,
+    private val bouncerInteractor: BouncerInteractor,
+    private val authenticationInteractor: AuthenticationInteractor,
+    private val devicePolicyManager: DevicePolicyManager,
+    private val bouncerMessageViewModelFactory: BouncerMessageViewModel.Factory,
+    private val flags: ComposeBouncerFlags,
+    private val userSwitcher: UserSwitcherViewModel,
+    private val actionButtonInteractor: BouncerActionButtonInteractor,
+    private val pinViewModelFactory: PinBouncerViewModel.Factory,
+    private val patternViewModelFactory: PatternBouncerViewModel.Factory,
+    private val passwordViewModelFactory: PasswordBouncerViewModel.Factory,
+) : SysUiViewModel() {
+    private val _selectedUserImage = MutableStateFlow<Bitmap?>(null)
+    val selectedUserImage: StateFlow<Bitmap?> = _selectedUserImage.asStateFlow()
+
+    val message: BouncerMessageViewModel by lazy { bouncerMessageViewModelFactory.create() }
+
+    private val _userSwitcherDropdown =
+        MutableStateFlow<List<UserSwitcherDropdownItemViewModel>>(emptyList())
+    val userSwitcherDropdown: StateFlow<List<UserSwitcherDropdownItemViewModel>> =
+        _userSwitcherDropdown.asStateFlow()
+
+    val isUserSwitcherVisible: Boolean
+        get() = bouncerInteractor.isUserSwitcherVisible
+
+    /** View-model for the current UI, based on the current authentication method. */
+    private val _authMethodViewModel = MutableStateFlow<AuthMethodBouncerViewModel?>(null)
+    val authMethodViewModel: StateFlow<AuthMethodBouncerViewModel?> =
+        _authMethodViewModel.asStateFlow()
+
+    /**
+     * A message for a dialog to show when the user has attempted the wrong credential too many
+     * times and now must wait a while before attempting again.
+     *
+     * If `null`, the lockout dialog should not be shown.
+     */
+    private val lockoutDialogMessage = MutableStateFlow<String?>(null)
+
+    /**
+     * A message for a dialog to show when the user has attempted the wrong credential too many
+     * times and their user/profile/device data is at risk of being wiped due to a Device Manager
+     * policy.
+     *
+     * If `null`, the wipe dialog should not be shown.
+     */
+    private val wipeDialogMessage = MutableStateFlow<String?>(null)
+
+    private val _dialogViewModel = MutableStateFlow<DialogViewModel?>(createDialogViewModel())
+    /**
+     * Models the dialog to be shown to the user, or `null` if no dialog should be shown.
+     *
+     * Once the dialog is shown, the UI should call [DialogViewModel.onDismiss] when the user
+     * dismisses this dialog.
+     */
+    val dialogViewModel: StateFlow<DialogViewModel?> = _dialogViewModel.asStateFlow()
+
+    private val _actionButton = MutableStateFlow<BouncerActionButtonModel?>(null)
+    /**
+     * The bouncer action button (Return to Call / Emergency Call). If `null`, the button should not
+     * be shown.
+     */
+    val actionButton: StateFlow<BouncerActionButtonModel?> = _actionButton.asStateFlow()
+
+    private val _isSideBySideSupported =
+        MutableStateFlow(isSideBySideSupported(authMethodViewModel.value))
+    /**
+     * Whether the "side-by-side" layout is supported.
+     *
+     * When presented on its own, without a user switcher (e.g. not on communal devices like
+     * tablets, for example), some authentication method UIs don't do well if they're shown in the
+     * side-by-side layout; these need to be shown with the standard layout so they can take up as
+     * much width as possible.
+     */
+    val isSideBySideSupported: StateFlow<Boolean> = _isSideBySideSupported.asStateFlow()
+
+    private val _isFoldSplitRequired =
+        MutableStateFlow(isFoldSplitRequired(authMethodViewModel.value))
+    /**
+     * Whether the splitting the UI around the fold seam (where the hinge is on a foldable device)
+     * is required.
+     */
+    val isFoldSplitRequired: StateFlow<Boolean> = _isFoldSplitRequired.asStateFlow()
+
+    private val _isInputEnabled =
+        MutableStateFlow(authenticationInteractor.lockoutEndTimestamp == null)
+    private val isInputEnabled: StateFlow<Boolean> = _isInputEnabled.asStateFlow()
+
+    override suspend fun onActivated() {
+        coroutineScope {
+            launch { message.activate() }
+            launch {
+                authenticationInteractor.authenticationMethod
+                    .map(::getChildViewModel)
+                    .collectLatest { childViewModelOrNull ->
+                        _authMethodViewModel.value = childViewModelOrNull
+                        childViewModelOrNull?.activate()
+                    }
+            }
+
+            launch {
+                authenticationInteractor.upcomingWipe.collect { wipeModel ->
+                    wipeDialogMessage.value = wipeModel?.message
+                }
+            }
+
+            launch {
+                userSwitcher.selectedUser
+                    .map { it.image.toBitmap() }
+                    .collectLatest { _selectedUserImage.value = it }
+            }
+
+            launch {
+                combine(
+                        userSwitcher.users,
+                        userSwitcher.menu,
+                    ) { users, actions ->
+                        users.map { user ->
+                            UserSwitcherDropdownItemViewModel(
+                                icon = Icon.Loaded(user.image, contentDescription = null),
+                                text = user.name,
+                                onClick = user.onClicked ?: {},
+                            )
+                        } +
+                            actions.map { action ->
+                                UserSwitcherDropdownItemViewModel(
+                                    icon =
+                                        Icon.Resource(
+                                            action.iconResourceId,
+                                            contentDescription = null
+                                        ),
+                                    text = Text.Resource(action.textResourceId),
+                                    onClick = action.onClicked,
+                                )
+                            }
+                    }
+                    .collectLatest { _userSwitcherDropdown.value = it }
+            }
+
+            launch {
+                combine(wipeDialogMessage, lockoutDialogMessage) { _, _ -> createDialogViewModel() }
+                    .collectLatest { _dialogViewModel.value = it }
+            }
+
+            launch {
+                actionButtonInteractor.actionButton.collectLatest { _actionButton.value = it }
+            }
+
+            launch {
+                authMethodViewModel
+                    .map { authMethod -> isSideBySideSupported(authMethod) }
+                    .collectLatest { _isSideBySideSupported.value = it }
+            }
+
+            launch {
+                authMethodViewModel
+                    .map { authMethod -> isFoldSplitRequired(authMethod) }
+                    .collectLatest { _isFoldSplitRequired.value = it }
+            }
+
+            launch {
+                message.isLockoutMessagePresent
+                    .map { lockoutMessagePresent -> !lockoutMessagePresent }
+                    .collectLatest { _isInputEnabled.value = it }
+            }
+        }
+    }
+
+    private fun isSideBySideSupported(authMethod: AuthMethodBouncerViewModel?): Boolean {
+        return isUserSwitcherVisible || authMethod !is PasswordBouncerViewModel
+    }
+
+    private fun isFoldSplitRequired(authMethod: AuthMethodBouncerViewModel?): Boolean {
+        return authMethod !is PasswordBouncerViewModel
+    }
+
+    private fun getChildViewModel(
+        authenticationMethod: AuthenticationMethodModel,
+    ): AuthMethodBouncerViewModel? {
+        // If the current child view-model matches the authentication method, reuse it instead of
+        // creating a new instance.
+        val childViewModel = authMethodViewModel.value
+        if (authenticationMethod == childViewModel?.authenticationMethod) {
+            return childViewModel
+        }
+
+        return when (authenticationMethod) {
+            is AuthenticationMethodModel.Pin ->
+                pinViewModelFactory.create(
+                    authenticationMethod = authenticationMethod,
+                    onIntentionalUserInput = ::onIntentionalUserInput,
+                    isInputEnabled = isInputEnabled,
+                )
+            is AuthenticationMethodModel.Sim ->
+                pinViewModelFactory.create(
+                    authenticationMethod = authenticationMethod,
+                    onIntentionalUserInput = ::onIntentionalUserInput,
+                    isInputEnabled = isInputEnabled,
+                )
+            is AuthenticationMethodModel.Password ->
+                passwordViewModelFactory.create(
+                    onIntentionalUserInput = ::onIntentionalUserInput,
+                    isInputEnabled = isInputEnabled,
+                )
+            is AuthenticationMethodModel.Pattern ->
+                patternViewModelFactory.create(
+                    onIntentionalUserInput = ::onIntentionalUserInput,
+                    isInputEnabled = isInputEnabled,
+                )
+            else -> null
+        }
+    }
+
+    private fun onIntentionalUserInput() {
+        message.showDefaultMessage()
+        bouncerInteractor.onIntentionalUserInput()
+    }
+
+    /**
+     * @return A message warning the user that the user/profile/device will be wiped upon a further
+     *   [AuthenticationWipeModel.remainingAttempts] unsuccessful authentication attempts.
+     */
+    private fun AuthenticationWipeModel.getAlmostAtWipeMessage(): String {
+        val message =
+            applicationContext.getString(
+                wipeTarget.messageIdForAlmostWipe,
+                failedAttempts,
+                remainingAttempts,
+            )
+        return if (wipeTarget == AuthenticationWipeModel.WipeTarget.ManagedProfile) {
+            devicePolicyManager.resources.getString(
+                DevicePolicyResources.Strings.SystemUi
+                    .KEYGUARD_DIALOG_FAILED_ATTEMPTS_ALMOST_ERASING_PROFILE,
+                { message },
+                failedAttempts,
+                remainingAttempts,
+            ) ?: message
+        } else {
+            message
+        }
+    }
+
+    /**
+     * @return A message informing the user that their user/profile/device will be wiped promptly.
+     */
+    private fun AuthenticationWipeModel.getWipeMessage(): String {
+        val message = applicationContext.getString(wipeTarget.messageIdForWipe, failedAttempts)
+        return if (wipeTarget == AuthenticationWipeModel.WipeTarget.ManagedProfile) {
+            devicePolicyManager.resources.getString(
+                DevicePolicyResources.Strings.SystemUi
+                    .KEYGUARD_DIALOG_FAILED_ATTEMPTS_ERASING_PROFILE,
+                { message },
+                failedAttempts,
+            ) ?: message
+        } else {
+            message
+        }
+    }
+
+    private val AuthenticationWipeModel.message: String
+        get() = if (remainingAttempts > 0) getAlmostAtWipeMessage() else getWipeMessage()
+
+    private fun createDialogViewModel(): DialogViewModel? {
+        val wipeText = wipeDialogMessage.value
+        val lockoutText = lockoutDialogMessage.value
+        return when {
+            // The wipe dialog takes priority over the lockout dialog.
+            wipeText != null ->
+                DialogViewModel(
+                    text = wipeText,
+                    onDismiss = { wipeDialogMessage.value = null },
+                )
+            lockoutText != null ->
+                DialogViewModel(
+                    text = lockoutText,
+                    onDismiss = { lockoutDialogMessage.value = null },
+                )
+            else -> null // No dialog to show.
+        }
+    }
+
+    /**
+     * Notifies that a key event has occurred.
+     *
+     * @return `true` when the [KeyEvent] was consumed as user input on bouncer; `false` otherwise.
+     */
+    fun onKeyEvent(keyEvent: KeyEvent): Boolean {
+        return (authMethodViewModel.value as? PinBouncerViewModel)?.onKeyEvent(
+            keyEvent.type,
+            keyEvent.nativeKeyEvent.keyCode
+        ) ?: false
+    }
+
+    data class DialogViewModel(
+        val text: String,
+
+        /** Callback to run after the dialog has been dismissed by the user. */
+        val onDismiss: () -> Unit,
+    )
+
+    data class UserSwitcherDropdownItemViewModel(
+        val icon: Icon,
+        val text: Text,
+        val onClick: () -> Unit,
+    )
+
+    @AssistedFactory
+    interface Factory {
+        fun create(): BouncerSceneContentViewModel
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt
deleted file mode 100644
index e2089bb..0000000
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt
+++ /dev/null
@@ -1,439 +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.bouncer.ui.viewmodel
-
-import android.app.admin.DevicePolicyManager
-import android.app.admin.DevicePolicyResources
-import android.content.Context
-import android.graphics.Bitmap
-import androidx.compose.ui.input.key.KeyEvent
-import androidx.compose.ui.input.key.type
-import androidx.core.graphics.drawable.toBitmap
-import com.android.compose.animation.scene.Back
-import com.android.compose.animation.scene.Swipe
-import com.android.compose.animation.scene.SwipeDirection
-import com.android.compose.animation.scene.UserAction
-import com.android.compose.animation.scene.UserActionResult
-import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
-import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
-import com.android.systemui.authentication.shared.model.AuthenticationWipeModel
-import com.android.systemui.bouncer.domain.interactor.BouncerActionButtonInteractor
-import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
-import com.android.systemui.bouncer.domain.interactor.SimBouncerInteractor
-import com.android.systemui.bouncer.shared.flag.ComposeBouncerFlags
-import com.android.systemui.bouncer.shared.model.BouncerActionButtonModel
-import com.android.systemui.common.shared.model.Icon
-import com.android.systemui.common.shared.model.Text
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.inputmethod.domain.interactor.InputMethodInteractor
-import com.android.systemui.user.domain.interactor.SelectedUserInteractor
-import com.android.systemui.user.ui.viewmodel.UserActionViewModel
-import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel
-import com.android.systemui.user.ui.viewmodel.UserViewModel
-import dagger.Module
-import dagger.Provides
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.SupervisorJob
-import kotlinx.coroutines.cancel
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.stateIn
-import kotlinx.coroutines.job
-import kotlinx.coroutines.launch
-
-/** Holds UI state and handles user input on bouncer UIs. */
-class BouncerViewModel(
-    @Application private val applicationContext: Context,
-    @Deprecated("TODO(b/354270224): remove this. Injecting CoroutineScope to view-models is banned")
-    @Application
-    private val applicationScope: CoroutineScope,
-    @Main private val mainDispatcher: CoroutineDispatcher,
-    private val bouncerInteractor: BouncerInteractor,
-    private val inputMethodInteractor: InputMethodInteractor,
-    private val simBouncerInteractor: SimBouncerInteractor,
-    private val authenticationInteractor: AuthenticationInteractor,
-    private val selectedUserInteractor: SelectedUserInteractor,
-    private val devicePolicyManager: DevicePolicyManager,
-    bouncerMessageViewModel: BouncerMessageViewModel,
-    flags: ComposeBouncerFlags,
-    selectedUser: Flow<UserViewModel>,
-    users: Flow<List<UserViewModel>>,
-    userSwitcherMenu: Flow<List<UserActionViewModel>>,
-    actionButton: Flow<BouncerActionButtonModel?>,
-) {
-    val selectedUserImage: StateFlow<Bitmap?> =
-        selectedUser
-            .map { it.image.toBitmap() }
-            .stateIn(
-                scope = applicationScope,
-                started = SharingStarted.WhileSubscribed(),
-                initialValue = null,
-            )
-
-    val destinationScenes: Flow<Map<UserAction, UserActionResult>> =
-        bouncerInteractor.dismissDestination.map { prevScene ->
-            mapOf(
-                Back to UserActionResult(prevScene),
-                Swipe(SwipeDirection.Down) to UserActionResult(prevScene),
-            )
-        }
-
-    val message: BouncerMessageViewModel = bouncerMessageViewModel
-
-    val userSwitcherDropdown: StateFlow<List<UserSwitcherDropdownItemViewModel>> =
-        combine(
-                users,
-                userSwitcherMenu,
-            ) { users, actions ->
-                users.map { user ->
-                    UserSwitcherDropdownItemViewModel(
-                        icon = Icon.Loaded(user.image, contentDescription = null),
-                        text = user.name,
-                        onClick = user.onClicked ?: {},
-                    )
-                } +
-                    actions.map { action ->
-                        UserSwitcherDropdownItemViewModel(
-                            icon = Icon.Resource(action.iconResourceId, contentDescription = null),
-                            text = Text.Resource(action.textResourceId),
-                            onClick = action.onClicked,
-                        )
-                    }
-            }
-            .stateIn(
-                scope = applicationScope,
-                started = SharingStarted.WhileSubscribed(),
-                initialValue = emptyList(),
-            )
-
-    val isUserSwitcherVisible: Boolean
-        get() = bouncerInteractor.isUserSwitcherVisible
-
-    // Handle to the scope of the child ViewModel (stored in [authMethod]).
-    private var childViewModelScope: CoroutineScope? = null
-
-    /** View-model for the current UI, based on the current authentication method. */
-    val authMethodViewModel: StateFlow<AuthMethodBouncerViewModel?> =
-        authenticationInteractor.authenticationMethod
-            .map(::getChildViewModel)
-            .stateIn(
-                scope = applicationScope,
-                started = SharingStarted.WhileSubscribed(),
-                initialValue = null,
-            )
-
-    /**
-     * A message for a dialog to show when the user has attempted the wrong credential too many
-     * times and now must wait a while before attempting again.
-     *
-     * If `null`, the lockout dialog should not be shown.
-     */
-    private val lockoutDialogMessage = MutableStateFlow<String?>(null)
-
-    /**
-     * A message for a dialog to show when the user has attempted the wrong credential too many
-     * times and their user/profile/device data is at risk of being wiped due to a Device Manager
-     * policy.
-     *
-     * If `null`, the wipe dialog should not be shown.
-     */
-    private val wipeDialogMessage = MutableStateFlow<String?>(null)
-
-    /**
-     * Models the dialog to be shown to the user, or `null` if no dialog should be shown.
-     *
-     * Once the dialog is shown, the UI should call [DialogViewModel.onDismiss] when the user
-     * dismisses this dialog.
-     */
-    val dialogViewModel: StateFlow<DialogViewModel?> =
-        combine(wipeDialogMessage, lockoutDialogMessage) { _, _ -> createDialogViewModel() }
-            .stateIn(
-                scope = applicationScope,
-                started = SharingStarted.WhileSubscribed(),
-                initialValue = createDialogViewModel(),
-            )
-
-    /**
-     * The bouncer action button (Return to Call / Emergency Call). If `null`, the button should not
-     * be shown.
-     */
-    val actionButton: StateFlow<BouncerActionButtonModel?> =
-        actionButton.stateIn(
-            scope = applicationScope,
-            started = SharingStarted.WhileSubscribed(),
-            initialValue = null
-        )
-
-    /**
-     * Whether the "side-by-side" layout is supported.
-     *
-     * When presented on its own, without a user switcher (e.g. not on communal devices like
-     * tablets, for example), some authentication method UIs don't do well if they're shown in the
-     * side-by-side layout; these need to be shown with the standard layout so they can take up as
-     * much width as possible.
-     */
-    val isSideBySideSupported: StateFlow<Boolean> =
-        authMethodViewModel
-            .map { authMethod -> isSideBySideSupported(authMethod) }
-            .stateIn(
-                scope = applicationScope,
-                started = SharingStarted.WhileSubscribed(),
-                initialValue = isSideBySideSupported(authMethodViewModel.value),
-            )
-
-    /**
-     * Whether the splitting the UI around the fold seam (where the hinge is on a foldable device)
-     * is required.
-     */
-    val isFoldSplitRequired: StateFlow<Boolean> =
-        authMethodViewModel
-            .map { authMethod -> isFoldSplitRequired(authMethod) }
-            .stateIn(
-                scope = applicationScope,
-                started = SharingStarted.WhileSubscribed(),
-                initialValue = isFoldSplitRequired(authMethodViewModel.value),
-            )
-
-    private val isInputEnabled: StateFlow<Boolean> =
-        bouncerMessageViewModel.isLockoutMessagePresent
-            .map { lockoutMessagePresent -> !lockoutMessagePresent }
-            .stateIn(
-                scope = applicationScope,
-                started = SharingStarted.WhileSubscribed(),
-                initialValue = authenticationInteractor.lockoutEndTimestamp == null,
-            )
-
-    init {
-        if (flags.isComposeBouncerOrSceneContainerEnabled()) {
-            // Keeps the upcoming wipe dialog up-to-date.
-            applicationScope.launch {
-                authenticationInteractor.upcomingWipe.collect { wipeModel ->
-                    wipeDialogMessage.value = wipeModel?.message
-                }
-            }
-        }
-    }
-
-    private fun isSideBySideSupported(authMethod: AuthMethodBouncerViewModel?): Boolean {
-        return isUserSwitcherVisible || authMethod !is PasswordBouncerViewModel
-    }
-
-    private fun isFoldSplitRequired(authMethod: AuthMethodBouncerViewModel?): Boolean {
-        return authMethod !is PasswordBouncerViewModel
-    }
-
-    private fun getChildViewModel(
-        authenticationMethod: AuthenticationMethodModel,
-    ): AuthMethodBouncerViewModel? {
-        // If the current child view-model matches the authentication method, reuse it instead of
-        // creating a new instance.
-        val childViewModel = authMethodViewModel.value
-        if (authenticationMethod == childViewModel?.authenticationMethod) {
-            return childViewModel
-        }
-
-        childViewModelScope?.cancel()
-        val newViewModelScope = createChildCoroutineScope(applicationScope)
-        childViewModelScope = newViewModelScope
-        return when (authenticationMethod) {
-            is AuthenticationMethodModel.Pin ->
-                PinBouncerViewModel(
-                    applicationContext = applicationContext,
-                    viewModelScope = newViewModelScope,
-                    interactor = bouncerInteractor,
-                    isInputEnabled = isInputEnabled,
-                    simBouncerInteractor = simBouncerInteractor,
-                    authenticationMethod = authenticationMethod,
-                    onIntentionalUserInput = ::onIntentionalUserInput
-                )
-            is AuthenticationMethodModel.Sim ->
-                PinBouncerViewModel(
-                    applicationContext = applicationContext,
-                    viewModelScope = newViewModelScope,
-                    interactor = bouncerInteractor,
-                    isInputEnabled = isInputEnabled,
-                    simBouncerInteractor = simBouncerInteractor,
-                    authenticationMethod = authenticationMethod,
-                    onIntentionalUserInput = ::onIntentionalUserInput
-                )
-            is AuthenticationMethodModel.Password ->
-                PasswordBouncerViewModel(
-                    viewModelScope = newViewModelScope,
-                    isInputEnabled = isInputEnabled,
-                    interactor = bouncerInteractor,
-                    inputMethodInteractor = inputMethodInteractor,
-                    selectedUserInteractor = selectedUserInteractor,
-                    onIntentionalUserInput = ::onIntentionalUserInput
-                )
-            is AuthenticationMethodModel.Pattern ->
-                PatternBouncerViewModel(
-                    applicationContext = applicationContext,
-                    viewModelScope = newViewModelScope,
-                    interactor = bouncerInteractor,
-                    isInputEnabled = isInputEnabled,
-                    onIntentionalUserInput = ::onIntentionalUserInput
-                )
-            else -> null
-        }
-    }
-
-    private fun onIntentionalUserInput() {
-        message.showDefaultMessage()
-        bouncerInteractor.onIntentionalUserInput()
-    }
-
-    private fun createChildCoroutineScope(parentScope: CoroutineScope): CoroutineScope {
-        return CoroutineScope(
-            SupervisorJob(parent = parentScope.coroutineContext.job) + mainDispatcher
-        )
-    }
-
-    /**
-     * @return A message warning the user that the user/profile/device will be wiped upon a further
-     *   [AuthenticationWipeModel.remainingAttempts] unsuccessful authentication attempts.
-     */
-    private fun AuthenticationWipeModel.getAlmostAtWipeMessage(): String {
-        val message =
-            applicationContext.getString(
-                wipeTarget.messageIdForAlmostWipe,
-                failedAttempts,
-                remainingAttempts,
-            )
-        return if (wipeTarget == AuthenticationWipeModel.WipeTarget.ManagedProfile) {
-            devicePolicyManager.resources.getString(
-                DevicePolicyResources.Strings.SystemUi
-                    .KEYGUARD_DIALOG_FAILED_ATTEMPTS_ALMOST_ERASING_PROFILE,
-                { message },
-                failedAttempts,
-                remainingAttempts,
-            ) ?: message
-        } else {
-            message
-        }
-    }
-
-    /**
-     * @return A message informing the user that their user/profile/device will be wiped promptly.
-     */
-    private fun AuthenticationWipeModel.getWipeMessage(): String {
-        val message = applicationContext.getString(wipeTarget.messageIdForWipe, failedAttempts)
-        return if (wipeTarget == AuthenticationWipeModel.WipeTarget.ManagedProfile) {
-            devicePolicyManager.resources.getString(
-                DevicePolicyResources.Strings.SystemUi
-                    .KEYGUARD_DIALOG_FAILED_ATTEMPTS_ERASING_PROFILE,
-                { message },
-                failedAttempts,
-            ) ?: message
-        } else {
-            message
-        }
-    }
-
-    private val AuthenticationWipeModel.message: String
-        get() = if (remainingAttempts > 0) getAlmostAtWipeMessage() else getWipeMessage()
-
-    private fun createDialogViewModel(): DialogViewModel? {
-        val wipeText = wipeDialogMessage.value
-        val lockoutText = lockoutDialogMessage.value
-        return when {
-            // The wipe dialog takes priority over the lockout dialog.
-            wipeText != null ->
-                DialogViewModel(
-                    text = wipeText,
-                    onDismiss = { wipeDialogMessage.value = null },
-                )
-            lockoutText != null ->
-                DialogViewModel(
-                    text = lockoutText,
-                    onDismiss = { lockoutDialogMessage.value = null },
-                )
-            else -> null // No dialog to show.
-        }
-    }
-
-    /**
-     * Notifies that a key event has occurred.
-     *
-     * @return `true` when the [KeyEvent] was consumed as user input on bouncer; `false` otherwise.
-     */
-    fun onKeyEvent(keyEvent: KeyEvent): Boolean {
-        return (authMethodViewModel.value as? PinBouncerViewModel)?.onKeyEvent(
-            keyEvent.type,
-            keyEvent.nativeKeyEvent.keyCode
-        ) ?: false
-    }
-
-    data class DialogViewModel(
-        val text: String,
-
-        /** Callback to run after the dialog has been dismissed by the user. */
-        val onDismiss: () -> Unit,
-    )
-
-    data class UserSwitcherDropdownItemViewModel(
-        val icon: Icon,
-        val text: Text,
-        val onClick: () -> Unit,
-    )
-}
-
-@Module
-object BouncerViewModelModule {
-
-    @Provides
-    @SysUISingleton
-    fun viewModel(
-        @Application applicationContext: Context,
-        @Application applicationScope: CoroutineScope,
-        @Main mainDispatcher: CoroutineDispatcher,
-        bouncerInteractor: BouncerInteractor,
-        imeInteractor: InputMethodInteractor,
-        simBouncerInteractor: SimBouncerInteractor,
-        actionButtonInteractor: BouncerActionButtonInteractor,
-        authenticationInteractor: AuthenticationInteractor,
-        selectedUserInteractor: SelectedUserInteractor,
-        flags: ComposeBouncerFlags,
-        userSwitcherViewModel: UserSwitcherViewModel,
-        devicePolicyManager: DevicePolicyManager,
-        bouncerMessageViewModel: BouncerMessageViewModel,
-    ): BouncerViewModel {
-        return BouncerViewModel(
-            applicationContext = applicationContext,
-            applicationScope = applicationScope,
-            mainDispatcher = mainDispatcher,
-            bouncerInteractor = bouncerInteractor,
-            inputMethodInteractor = imeInteractor,
-            simBouncerInteractor = simBouncerInteractor,
-            authenticationInteractor = authenticationInteractor,
-            selectedUserInteractor = selectedUserInteractor,
-            devicePolicyManager = devicePolicyManager,
-            bouncerMessageViewModel = bouncerMessageViewModel,
-            flags = flags,
-            selectedUser = userSwitcherViewModel.selectedUser,
-            users = userSwitcherViewModel.users,
-            userSwitcherMenu = userSwitcherViewModel.menu,
-            actionButton = actionButtonInteractor.actionButton,
-        )
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt
index 052fb6b..9ead7a0 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt
@@ -23,29 +23,33 @@
 import com.android.systemui.res.R
 import com.android.systemui.user.domain.interactor.SelectedUserInteractor
 import com.android.systemui.util.kotlin.onSubscriberAdded
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
 import kotlin.time.Duration.Companion.milliseconds
-import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.collectLatest
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.receiveAsFlow
 import kotlinx.coroutines.launch
 
 /** Holds UI state and handles user input for the password bouncer UI. */
-class PasswordBouncerViewModel(
-    viewModelScope: CoroutineScope,
-    isInputEnabled: StateFlow<Boolean>,
+class PasswordBouncerViewModel
+@AssistedInject
+constructor(
     interactor: BouncerInteractor,
-    private val onIntentionalUserInput: () -> Unit,
     private val inputMethodInteractor: InputMethodInteractor,
     private val selectedUserInteractor: SelectedUserInteractor,
+    @Assisted isInputEnabled: StateFlow<Boolean>,
+    @Assisted private val onIntentionalUserInput: () -> Unit,
 ) :
     AuthMethodBouncerViewModel(
-        viewModelScope = viewModelScope,
         interactor = interactor,
         isInputEnabled = isInputEnabled,
     ) {
@@ -59,28 +63,70 @@
 
     override val lockoutMessageId = R.string.kg_too_many_failed_password_attempts_dialog_message
 
+    private val _isImeSwitcherButtonVisible = MutableStateFlow(false)
     /** Informs the UI whether the input method switcher button should be visible. */
-    val isImeSwitcherButtonVisible: StateFlow<Boolean> = imeSwitcherRefreshingFlow()
+    val isImeSwitcherButtonVisible: StateFlow<Boolean> = _isImeSwitcherButtonVisible.asStateFlow()
 
     /** Whether the text field element currently has focus. */
     private val isTextFieldFocused = MutableStateFlow(false)
 
+    private val _isTextFieldFocusRequested =
+        MutableStateFlow(isInputEnabled.value && !isTextFieldFocused.value)
     /** Whether the UI should request focus on the text field element. */
-    val isTextFieldFocusRequested =
-        combine(isInputEnabled, isTextFieldFocused) { hasInput, hasFocus -> hasInput && !hasFocus }
-            .stateIn(
-                scope = viewModelScope,
-                started = SharingStarted.WhileSubscribed(),
-                initialValue = isInputEnabled.value && !isTextFieldFocused.value,
-            )
+    val isTextFieldFocusRequested = _isTextFieldFocusRequested.asStateFlow()
 
+    private val _selectedUserId = MutableStateFlow(selectedUserInteractor.getSelectedUserId())
     /** The ID of the currently-selected user. */
-    val selectedUserId: StateFlow<Int> =
-        selectedUserInteractor.selectedUser.stateIn(
-            scope = viewModelScope,
-            started = SharingStarted.WhileSubscribed(),
-            initialValue = selectedUserInteractor.getSelectedUserId(),
-        )
+    val selectedUserId: StateFlow<Int> = _selectedUserId.asStateFlow()
+
+    private val requests = Channel<Request>(Channel.BUFFERED)
+
+    override suspend fun onActivated() {
+        coroutineScope {
+            launch { super.onActivated() }
+            launch {
+                requests.receiveAsFlow().collect { request ->
+                    when (request) {
+                        is OnImeSwitcherButtonClicked -> {
+                            inputMethodInteractor.showInputMethodPicker(
+                                displayId = request.displayId,
+                                showAuxiliarySubtypes = false,
+                            )
+                        }
+                        is OnImeDismissed -> {
+                            interactor.onImeHiddenByUser()
+                        }
+                    }
+                }
+            }
+            launch {
+                combine(isInputEnabled, isTextFieldFocused) { hasInput, hasFocus ->
+                        hasInput && !hasFocus
+                    }
+                    .collectLatest { _isTextFieldFocusRequested.value = it }
+            }
+            launch {
+                selectedUserInteractor.selectedUser.collectLatest { _selectedUserId.value = it }
+            }
+            launch {
+                // Re-fetch the currently-enabled IMEs whenever the selected user changes, and
+                // whenever
+                // the UI subscribes to the `isImeSwitcherButtonVisible` flow.
+                combine(
+                        // InputMethodManagerService sometimes takes some time to update its
+                        // internal
+                        // state when the selected user changes. As a workaround, delay fetching the
+                        // IME
+                        // info.
+                        selectedUserInteractor.selectedUser.onEach { delay(DELAY_TO_FETCH_IMES) },
+                        _isImeSwitcherButtonVisible.onSubscriberAdded()
+                    ) { selectedUserId, _ ->
+                        inputMethodInteractor.hasMultipleEnabledImesOrSubtypes(selectedUserId)
+                    }
+                    .collectLatest { _isImeSwitcherButtonVisible.value = it }
+            }
+        }
+    }
 
     override fun onHidden() {
         super.onHidden()
@@ -106,9 +152,7 @@
 
     /** Notifies that the user clicked the button to change the input method. */
     fun onImeSwitcherButtonClicked(displayId: Int) {
-        viewModelScope.launch {
-            inputMethodInteractor.showInputMethodPicker(displayId, showAuxiliarySubtypes = false)
-        }
+        requests.trySend(OnImeSwitcherButtonClicked(displayId))
     }
 
     /** Notifies that the user has pressed the key for attempting to authenticate the password. */
@@ -120,7 +164,7 @@
 
     /** Notifies that the user has dismissed the software keyboard (IME). */
     fun onImeDismissed() {
-        viewModelScope.launch { interactor.onImeHiddenByUser() }
+        requests.trySend(OnImeDismissed)
     }
 
     /** Notifies that the password text field has gained or lost focus. */
@@ -128,34 +172,21 @@
         isTextFieldFocused.value = isFocused
     }
 
-    /**
-     * Whether the input method switcher button should be displayed in the password bouncer UI. The
-     * value may be stale at the moment of subscription to this flow, but it is guaranteed to be
-     * shortly updated with a fresh value.
-     *
-     * Note: Each added subscription triggers an IPC call in the background, so this should only be
-     * subscribed to by the UI once in its lifecycle (i.e. when the bouncer is shown).
-     */
-    private fun imeSwitcherRefreshingFlow(): StateFlow<Boolean> {
-        val isImeSwitcherButtonVisible = MutableStateFlow(value = false)
-        viewModelScope.launch {
-            // Re-fetch the currently-enabled IMEs whenever the selected user changes, and whenever
-            // the UI subscribes to the `isImeSwitcherButtonVisible` flow.
-            combine(
-                    // InputMethodManagerService sometimes takes some time to update its internal
-                    // state when the selected user changes. As a workaround, delay fetching the IME
-                    // info.
-                    selectedUserInteractor.selectedUser.onEach { delay(DELAY_TO_FETCH_IMES) },
-                    isImeSwitcherButtonVisible.onSubscriberAdded()
-                ) { selectedUserId, _ ->
-                    inputMethodInteractor.hasMultipleEnabledImesOrSubtypes(selectedUserId)
-                }
-                .collect { isImeSwitcherButtonVisible.value = it }
-        }
-        return isImeSwitcherButtonVisible.asStateFlow()
+    @AssistedFactory
+    interface Factory {
+        fun create(
+            isInputEnabled: StateFlow<Boolean>,
+            onIntentionalUserInput: () -> Unit,
+        ): PasswordBouncerViewModel
     }
 
     companion object {
         @VisibleForTesting val DELAY_TO_FETCH_IMES = 300.milliseconds
     }
+
+    private sealed interface Request
+
+    private data class OnImeSwitcherButtonClicked(val displayId: Int) : Request
+
+    private data object OnImeDismissed : Request
 }
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt
index 8b9c0a9a..b1df04b 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt
@@ -22,28 +22,32 @@
 import com.android.systemui.authentication.shared.model.AuthenticationPatternCoordinate
 import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
 import com.android.systemui.res.R
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
 import kotlin.math.max
 import kotlin.math.min
 import kotlin.math.pow
 import kotlin.math.sqrt
-import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.collectLatest
 import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.launch
 
 /** Holds UI state and handles user input for the pattern bouncer UI. */
-class PatternBouncerViewModel(
+class PatternBouncerViewModel
+@AssistedInject
+constructor(
     private val applicationContext: Context,
-    viewModelScope: CoroutineScope,
     interactor: BouncerInteractor,
-    isInputEnabled: StateFlow<Boolean>,
-    private val onIntentionalUserInput: () -> Unit,
+    @Assisted isInputEnabled: StateFlow<Boolean>,
+    @Assisted private val onIntentionalUserInput: () -> Unit,
 ) :
     AuthMethodBouncerViewModel(
-        viewModelScope = viewModelScope,
         interactor = interactor,
         isInputEnabled = isInputEnabled,
     ) {
@@ -54,17 +58,10 @@
     /** The number of rows in the dot grid. */
     val rowCount = 3
 
-    private val _selectedDots = MutableStateFlow<LinkedHashSet<PatternDotViewModel>>(linkedSetOf())
-
+    private val selectedDotSet = MutableStateFlow<LinkedHashSet<PatternDotViewModel>>(linkedSetOf())
+    private val selectedDotList = MutableStateFlow(selectedDotSet.value.toList())
     /** The dots that were selected by the user, in the order of selection. */
-    val selectedDots: StateFlow<List<PatternDotViewModel>> =
-        _selectedDots
-            .map { it.toList() }
-            .stateIn(
-                scope = viewModelScope,
-                started = SharingStarted.WhileSubscribed(),
-                initialValue = emptyList(),
-            )
+    val selectedDots: StateFlow<List<PatternDotViewModel>> = selectedDotList.asStateFlow()
 
     private val _currentDot = MutableStateFlow<PatternDotViewModel?>(null)
 
@@ -83,6 +80,17 @@
 
     override val lockoutMessageId = R.string.kg_too_many_failed_pattern_attempts_dialog_message
 
+    override suspend fun onActivated() {
+        coroutineScope {
+            launch { super.onActivated() }
+            launch {
+                selectedDotSet
+                    .map { it.toList() }
+                    .collectLatest { selectedDotList.value = it.toList() }
+            }
+        }
+    }
+
     /** Notifies that the user has started a drag gesture across the dot grid. */
     fun onDragStart() {
         onIntentionalUserInput()
@@ -120,7 +128,7 @@
         }
 
         val hitDot = dots.value.firstOrNull { dot -> dot.x == dotColumn && dot.y == dotRow }
-        if (hitDot != null && !_selectedDots.value.contains(hitDot)) {
+        if (hitDot != null && !selectedDotSet.value.contains(hitDot)) {
             val skippedOverDots =
                 currentDot.value?.let { previousDot ->
                     buildList {
@@ -147,9 +155,9 @@
                     }
                 } ?: emptyList()
 
-            _selectedDots.value =
+            selectedDotSet.value =
                 linkedSetOf<PatternDotViewModel>().apply {
-                    addAll(_selectedDots.value)
+                    addAll(selectedDotSet.value)
                     addAll(skippedOverDots)
                     add(hitDot)
                 }
@@ -172,11 +180,11 @@
     override fun clearInput() {
         _dots.value = defaultDots()
         _currentDot.value = null
-        _selectedDots.value = linkedSetOf()
+        selectedDotSet.value = linkedSetOf()
     }
 
     override fun getInput(): List<Any> {
-        return _selectedDots.value.map(PatternDotViewModel::toCoordinate)
+        return selectedDotSet.value.map(PatternDotViewModel::toCoordinate)
     }
 
     private fun defaultDots(): List<PatternDotViewModel> {
@@ -204,6 +212,14 @@
         max(min(outValue.float, 1f), MIN_DOT_HIT_FACTOR)
     }
 
+    @AssistedFactory
+    interface Factory {
+        fun create(
+            isInputEnabled: StateFlow<Boolean>,
+            onIntentionalUserInput: () -> Unit,
+        ): PatternBouncerViewModel
+    }
+
     companion object {
         private const val MIN_DOT_HIT_FACTOR = 0.2f
     }
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
index aa447ff..cb36560 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
@@ -32,29 +32,34 @@
 import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
 import com.android.systemui.bouncer.domain.interactor.SimBouncerInteractor
 import com.android.systemui.res.R
-import kotlinx.coroutines.CoroutineScope
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.collectLatest
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.receiveAsFlow
 import kotlinx.coroutines.launch
 
 /** Holds UI state and handles user input for the PIN code bouncer UI. */
-class PinBouncerViewModel(
+class PinBouncerViewModel
+@AssistedInject
+constructor(
     applicationContext: Context,
-    viewModelScope: CoroutineScope,
     interactor: BouncerInteractor,
-    isInputEnabled: StateFlow<Boolean>,
-    private val onIntentionalUserInput: () -> Unit,
     private val simBouncerInteractor: SimBouncerInteractor,
-    authenticationMethod: AuthenticationMethodModel,
+    @Assisted isInputEnabled: StateFlow<Boolean>,
+    @Assisted private val onIntentionalUserInput: () -> Unit,
+    @Assisted override val authenticationMethod: AuthenticationMethodModel,
 ) :
     AuthMethodBouncerViewModel(
-        viewModelScope = viewModelScope,
         interactor = interactor,
         isInputEnabled = isInputEnabled,
     ) {
@@ -73,69 +78,89 @@
     /** Currently entered pin keys. */
     val pinInput: StateFlow<PinInputViewModel> = mutablePinInput
 
+    private val _hintedPinLength = MutableStateFlow<Int?>(null)
     /** The length of the PIN for which we should show a hint. */
-    val hintedPinLength: StateFlow<Int?> =
-        if (isSimAreaVisible) {
-                flowOf(null)
-            } else {
-                interactor.hintedPinLength
-            }
-            .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
+    val hintedPinLength: StateFlow<Int?> = _hintedPinLength.asStateFlow()
 
+    private val _backspaceButtonAppearance = MutableStateFlow(ActionButtonAppearance.Hidden)
     /** Appearance of the backspace button. */
     val backspaceButtonAppearance: StateFlow<ActionButtonAppearance> =
-        combine(
-                mutablePinInput,
-                interactor.isAutoConfirmEnabled,
-            ) { mutablePinEntries, isAutoConfirmEnabled ->
-                computeBackspaceButtonAppearance(
-                    pinInput = mutablePinEntries,
-                    isAutoConfirmEnabled = isAutoConfirmEnabled,
-                )
-            }
-            .stateIn(
-                scope = viewModelScope,
-                // Make sure this is kept as WhileSubscribed or we can run into a bug where the
-                // downstream continues to receive old/stale/cached values.
-                started = SharingStarted.WhileSubscribed(),
-                initialValue = ActionButtonAppearance.Hidden,
-            )
+        _backspaceButtonAppearance.asStateFlow()
 
+    private val _confirmButtonAppearance = MutableStateFlow(ActionButtonAppearance.Hidden)
     /** Appearance of the confirm button. */
     val confirmButtonAppearance: StateFlow<ActionButtonAppearance> =
-        interactor.isAutoConfirmEnabled
-            .map { if (it) ActionButtonAppearance.Hidden else ActionButtonAppearance.Shown }
-            .stateIn(
-                scope = viewModelScope,
-                started = SharingStarted.WhileSubscribed(),
-                initialValue = ActionButtonAppearance.Hidden,
-            )
-
-    override val authenticationMethod: AuthenticationMethodModel = authenticationMethod
+        _confirmButtonAppearance.asStateFlow()
 
     override val lockoutMessageId = R.string.kg_too_many_failed_pin_attempts_dialog_message
 
-    init {
-        viewModelScope.launch { simBouncerInteractor.subId.collect { onResetSimFlow() } }
+    private val requests = Channel<Request>(Channel.BUFFERED)
+
+    override suspend fun onActivated() {
+        coroutineScope {
+            launch { super.onActivated() }
+            launch {
+                requests.receiveAsFlow().collect { request ->
+                    when (request) {
+                        is OnErrorDialogDismissed -> {
+                            simBouncerInteractor.onErrorDialogDismissed()
+                        }
+                        is OnAuthenticateButtonClickedForSim -> {
+                            isSimUnlockingDialogVisible.value = true
+                            simBouncerInteractor.verifySim(getInput())
+                            isSimUnlockingDialogVisible.value = false
+                            clearInput()
+                        }
+                    }
+                }
+            }
+            launch { simBouncerInteractor.subId.collect { onResetSimFlow() } }
+            launch {
+                if (isSimAreaVisible) {
+                        flowOf(null)
+                    } else {
+                        interactor.hintedPinLength
+                    }
+                    .collectLatest { _hintedPinLength.value = it }
+            }
+            launch {
+                combine(
+                        mutablePinInput,
+                        interactor.isAutoConfirmEnabled,
+                    ) { mutablePinEntries, isAutoConfirmEnabled ->
+                        computeBackspaceButtonAppearance(
+                            pinInput = mutablePinEntries,
+                            isAutoConfirmEnabled = isAutoConfirmEnabled,
+                        )
+                    }
+                    .collectLatest { _backspaceButtonAppearance.value = it }
+            }
+            launch {
+                interactor.isAutoConfirmEnabled
+                    .map { if (it) ActionButtonAppearance.Hidden else ActionButtonAppearance.Shown }
+                    .collectLatest { _confirmButtonAppearance.value = it }
+            }
+            launch {
+                interactor.isPinEnhancedPrivacyEnabled
+                    .map { !it }
+                    .collectLatest { _isDigitButtonAnimationEnabled.value = it }
+            }
+        }
     }
 
     /** Notifies that the user dismissed the sim pin error dialog. */
     fun onErrorDialogDismissed() {
-        viewModelScope.launch { simBouncerInteractor.onErrorDialogDismissed() }
+        requests.trySend(OnErrorDialogDismissed)
     }
 
+    private val _isDigitButtonAnimationEnabled =
+        MutableStateFlow(!interactor.isPinEnhancedPrivacyEnabled.value)
     /**
      * Whether the digit buttons should be animated when touched. Note that this doesn't affect the
      * delete or enter buttons; those should always animate.
      */
     val isDigitButtonAnimationEnabled: StateFlow<Boolean> =
-        interactor.isPinEnhancedPrivacyEnabled
-            .map { !it }
-            .stateIn(
-                scope = viewModelScope,
-                started = SharingStarted.WhileSubscribed(),
-                initialValue = !interactor.isPinEnhancedPrivacyEnabled.value,
-            )
+        _isDigitButtonAnimationEnabled.asStateFlow()
 
     /** Notifies that the user clicked on a PIN button with the given digit value. */
     fun onPinButtonClicked(input: Int) {
@@ -163,19 +188,14 @@
     /** Notifies that the user clicked the "enter" button. */
     fun onAuthenticateButtonClicked() {
         if (authenticationMethod == AuthenticationMethodModel.Sim) {
-            viewModelScope.launch {
-                isSimUnlockingDialogVisible.value = true
-                simBouncerInteractor.verifySim(getInput())
-                isSimUnlockingDialogVisible.value = false
-                clearInput()
-            }
+            requests.trySend(OnAuthenticateButtonClickedForSim)
         } else {
             tryAuthenticate(useAutoConfirm = false)
         }
     }
 
     fun onDisableEsimButtonClicked() {
-        viewModelScope.launch { simBouncerInteractor.disableEsim() }
+        simBouncerInteractor.disableEsim()
     }
 
     /** Resets the sim screen and shows a default message. */
@@ -242,6 +262,21 @@
             else -> false
         }
     }
+
+    @AssistedFactory
+    interface Factory {
+        fun create(
+            isInputEnabled: StateFlow<Boolean>,
+            onIntentionalUserInput: () -> Unit,
+            authenticationMethod: AuthenticationMethodModel,
+        ): PinBouncerViewModel
+    }
+
+    private sealed interface Request
+
+    private data object OnErrorDialogDismissed : Request
+
+    private data object OnAuthenticateButtonClickedForSim : Request
 }
 
 /** Appearance of pin-pad action buttons. */
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/WidgetTrampolineInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/WidgetTrampolineInteractor.kt
new file mode 100644
index 0000000..7453368
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/WidgetTrampolineInteractor.kt
@@ -0,0 +1,140 @@
+/*
+ * 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.communal.domain.interactor
+
+import android.app.ActivityManager
+import com.android.systemui.common.usagestats.domain.UsageStatsInteractor
+import com.android.systemui.common.usagestats.shared.model.ActivityEventModel
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.Logger
+import com.android.systemui.log.dagger.CommunalLog
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.shared.system.TaskStackChangeListener
+import com.android.systemui.shared.system.TaskStackChangeListeners
+import com.android.systemui.util.kotlin.race
+import com.android.systemui.util.time.SystemClock
+import javax.inject.Inject
+import kotlin.time.Duration.Companion.milliseconds
+import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.TimeoutCancellationException
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.takeWhile
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.withTimeout
+
+/**
+ * Detects activity starts that occur while the communal hub is showing, within a short delay of a
+ * widget interaction occurring. Used for detecting non-activity trampolines which otherwise would
+ * not prompt the user for authentication.
+ */
+@OptIn(ExperimentalCoroutinesApi::class)
+@SysUISingleton
+class WidgetTrampolineInteractor
+@Inject
+constructor(
+    private val activityStarter: ActivityStarter,
+    private val systemClock: SystemClock,
+    private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
+    private val taskStackChangeListeners: TaskStackChangeListeners,
+    private val usageStatsInteractor: UsageStatsInteractor,
+    @CommunalLog logBuffer: LogBuffer,
+) {
+    private companion object {
+        const val TAG = "WidgetTrampolineInteractor"
+    }
+
+    private val logger = Logger(logBuffer, TAG)
+
+    /** Waits for a new task to be moved to the foreground. */
+    private suspend fun waitForNewForegroundTask() = suspendCancellableCoroutine { cont ->
+        val listener =
+            object : TaskStackChangeListener {
+                override fun onTaskMovedToFront(taskInfo: ActivityManager.RunningTaskInfo) {
+                    if (!cont.isCompleted) {
+                        cont.resume(Unit, null)
+                    }
+                }
+            }
+        taskStackChangeListeners.registerTaskStackListener(listener)
+        cont.invokeOnCancellation { taskStackChangeListeners.unregisterTaskStackListener(listener) }
+    }
+
+    /**
+     * Waits for an activity to enter a [ActivityEventModel.Lifecycle.RESUMED] state by periodically
+     * polling the system to see if any activities have started.
+     */
+    private suspend fun waitForActivityStartByPolling(startTime: Long): Boolean {
+        while (true) {
+            val events = usageStatsInteractor.queryActivityEvents(startTime = startTime)
+            if (events.any { event -> event.lifecycle == ActivityEventModel.Lifecycle.RESUMED }) {
+                return true
+            } else {
+                // Poll again in the future to check if an activity started.
+                delay(200.milliseconds)
+            }
+        }
+    }
+
+    /** Waits for a transition away from the hub to occur. */
+    private suspend fun waitForTransitionAwayFromHub() {
+        keyguardTransitionInteractor
+            .isFinishedIn(Scenes.Communal, KeyguardState.GLANCEABLE_HUB)
+            .takeWhile { it }
+            .collect {}
+    }
+
+    private suspend fun waitForActivityStartWhileOnHub(): Boolean {
+        val startTime = systemClock.currentTimeMillis()
+        return try {
+            return withTimeout(1.seconds) {
+                race(
+                    {
+                        waitForNewForegroundTask()
+                        true
+                    },
+                    { waitForActivityStartByPolling(startTime) },
+                    {
+                        waitForTransitionAwayFromHub()
+                        false
+                    },
+                )
+            }
+        } catch (e: TimeoutCancellationException) {
+            false
+        }
+    }
+
+    /**
+     * Checks if an activity starts while on the glanceable hub and dismisses the keyguard if it
+     * does. This can detect activities started due to broadcast trampolines from widgets.
+     */
+    suspend fun waitForActivityStartAndDismissKeyguard() {
+        if (waitForActivityStartWhileOnHub()) {
+            logger.d("Detected trampoline, requesting unlock")
+            activityStarter.dismissKeyguardThenExecute(
+                /* action= */ { false },
+                /* cancel= */ null,
+                /* afterKeyguardGone= */ false
+            )
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/smartspace/SmartspaceInteractionHandler.kt b/packages/SystemUI/src/com/android/systemui/communal/smartspace/SmartspaceInteractionHandler.kt
index c4edcac..99e3232 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/smartspace/SmartspaceInteractionHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/smartspace/SmartspaceInteractionHandler.kt
@@ -48,7 +48,17 @@
         InteractionHandlerDelegate(
             communalSceneInteractor,
             findViewToAnimate = { view -> view is SmartspaceAppWidgetHostView },
-            intentStarter = this::startIntent,
+            intentStarter =
+                object : InteractionHandlerDelegate.IntentStarter {
+                    override fun startActivity(
+                        intent: PendingIntent,
+                        fillInIntent: Intent,
+                        activityOptions: ActivityOptions,
+                        controller: ActivityTransitionAnimator.Controller?
+                    ): Boolean {
+                        return startIntent(intent, fillInIntent, activityOptions, controller)
+                    }
+                },
             logger = Logger(logBuffer, TAG),
         )
 
diff --git a/packages/SystemUI/src/com/android/systemui/communal/util/InteractionHandlerDelegate.kt b/packages/SystemUI/src/com/android/systemui/communal/util/InteractionHandlerDelegate.kt
index d2029d5..5e21afa 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/util/InteractionHandlerDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/util/InteractionHandlerDelegate.kt
@@ -19,6 +19,7 @@
 import android.app.ActivityOptions
 import android.app.PendingIntent
 import android.content.Intent
+import android.util.Pair as UtilPair
 import android.view.View
 import android.widget.RemoteViews
 import androidx.core.util.component1
@@ -36,14 +37,28 @@
     private val logger: Logger,
 ) : RemoteViews.InteractionHandler {
 
-    /** Responsible for starting the pending intent for launching activities. */
-    fun interface IntentStarter {
-        fun startPendingIntent(
+    interface IntentStarter {
+        /** Responsible for starting the pending intent for launching activities. */
+        fun startActivity(
             intent: PendingIntent,
             fillInIntent: Intent,
             activityOptions: ActivityOptions,
             controller: ActivityTransitionAnimator.Controller?,
         ): Boolean
+
+        /** Responsible for starting the pending intent for non-activity launches. */
+        fun startPendingIntent(
+            view: View,
+            pendingIntent: PendingIntent,
+            fillInIntent: Intent,
+            activityOptions: ActivityOptions,
+        ): Boolean {
+            return RemoteViews.startPendingIntent(
+                view,
+                pendingIntent,
+                UtilPair(fillInIntent, activityOptions),
+            )
+        }
     }
 
     override fun onInteraction(
@@ -55,7 +70,7 @@
             str1 = pendingIntent.toLoggingString()
             str2 = pendingIntent.creatorPackage
         }
-        val launchOptions = response.getLaunchOptions(view)
+        val (fillInIntent, activityOptions) = response.getLaunchOptions(view)
         return when {
             pendingIntent.isActivity -> {
                 // Forward the fill-in intent and activity options retrieved from the response
@@ -67,15 +82,15 @@
                         communalSceneInteractor.setIsLaunchingWidget(true)
                         CommunalTransitionAnimatorController(it, communalSceneInteractor)
                     }
-                val (fillInIntent, activityOptions) = launchOptions
-                intentStarter.startPendingIntent(
+                intentStarter.startActivity(
                     pendingIntent,
                     fillInIntent,
                     activityOptions,
                     animationController
                 )
             }
-            else -> RemoteViews.startPendingIntent(view, pendingIntent, launchOptions)
+            else ->
+                intentStarter.startPendingIntent(view, pendingIntent, fillInIntent, activityOptions)
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetInteractionHandler.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetInteractionHandler.kt
index 0eeb506..121b4a3 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetInteractionHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetInteractionHandler.kt
@@ -21,22 +21,30 @@
 import android.content.Intent
 import android.view.View
 import android.widget.RemoteViews
+import com.android.app.tracing.coroutines.launch
+import com.android.systemui.Flags.communalWidgetTrampolineFix
 import com.android.systemui.animation.ActivityTransitionAnimator
 import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
+import com.android.systemui.communal.domain.interactor.WidgetTrampolineInteractor
 import com.android.systemui.communal.util.InteractionHandlerDelegate
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.core.Logger
 import com.android.systemui.log.dagger.CommunalLog
 import com.android.systemui.plugins.ActivityStarter
 import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
 
 @SysUISingleton
 class WidgetInteractionHandler
 @Inject
 constructor(
+    @Application applicationScope: CoroutineScope,
     private val activityStarter: ActivityStarter,
     communalSceneInteractor: CommunalSceneInteractor,
+    private val widgetTrampolineInteractor: WidgetTrampolineInteractor,
     @CommunalLog val logBuffer: LogBuffer,
 ) : RemoteViews.InteractionHandler {
 
@@ -48,7 +56,52 @@
         InteractionHandlerDelegate(
             communalSceneInteractor,
             findViewToAnimate = { view -> view is CommunalAppWidgetHostView },
-            intentStarter = this::startIntent,
+            intentStarter =
+                object : InteractionHandlerDelegate.IntentStarter {
+                    private var job: Job? = null
+
+                    override fun startActivity(
+                        intent: PendingIntent,
+                        fillInIntent: Intent,
+                        activityOptions: ActivityOptions,
+                        controller: ActivityTransitionAnimator.Controller?
+                    ): Boolean {
+                        cancelTrampolineMonitoring()
+                        return startActivityIntent(
+                            intent,
+                            fillInIntent,
+                            activityOptions,
+                            controller
+                        )
+                    }
+
+                    override fun startPendingIntent(
+                        view: View,
+                        pendingIntent: PendingIntent,
+                        fillInIntent: Intent,
+                        activityOptions: ActivityOptions
+                    ): Boolean {
+                        cancelTrampolineMonitoring()
+                        if (communalWidgetTrampolineFix()) {
+                            job =
+                                applicationScope.launch("$TAG#monitorForActivityStart") {
+                                    widgetTrampolineInteractor
+                                        .waitForActivityStartAndDismissKeyguard()
+                                }
+                        }
+                        return super.startPendingIntent(
+                            view,
+                            pendingIntent,
+                            fillInIntent,
+                            activityOptions
+                        )
+                    }
+
+                    private fun cancelTrampolineMonitoring() {
+                        job?.cancel()
+                        job = null
+                    }
+                },
             logger = Logger(logBuffer, TAG),
         )
 
@@ -58,7 +111,7 @@
         response: RemoteViews.RemoteResponse
     ): Boolean = delegate.onInteraction(view, pendingIntent, response)
 
-    private fun startIntent(
+    private fun startActivityIntent(
         pendingIntent: PendingIntent,
         fillInIntent: Intent,
         extraOptions: ActivityOptions,
diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/data/repository/TutorialSchedulerRepository.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/data/repository/TutorialSchedulerRepository.kt
index b9b3895..36b9ac7 100644
--- a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/data/repository/TutorialSchedulerRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/data/repository/TutorialSchedulerRepository.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.inputdevice.tutorial.data.repository
 
 import android.content.Context
+import androidx.annotation.VisibleForTesting
 import androidx.datastore.core.DataStore
 import androidx.datastore.preferences.core.Preferences
 import androidx.datastore.preferences.core.booleanPreferencesKey
@@ -33,15 +34,19 @@
 import kotlinx.coroutines.flow.map
 
 @SysUISingleton
-class TutorialSchedulerRepository
-@Inject
-constructor(
-    @Application private val applicationContext: Context,
-    @Background private val backgroundScope: CoroutineScope
+class TutorialSchedulerRepository(
+    private val applicationContext: Context,
+    backgroundScope: CoroutineScope,
+    dataStoreName: String
 ) {
+    @Inject
+    constructor(
+        @Application applicationContext: Context,
+        @Background backgroundScope: CoroutineScope
+    ) : this(applicationContext, backgroundScope, dataStoreName = "TutorialScheduler")
 
     private val Context.dataStore: DataStore<Preferences> by
-        preferencesDataStore(name = DATASTORE_NAME, scope = backgroundScope)
+        preferencesDataStore(name = dataStoreName, scope = backgroundScope)
 
     suspend fun isLaunched(deviceType: DeviceType): Boolean = loadData()[deviceType]!!.isLaunched
 
@@ -81,8 +86,12 @@
     private fun getConnectKey(device: DeviceType) =
         longPreferencesKey(device.name + CONNECT_TIME_SUFFIX)
 
+    @VisibleForTesting
+    suspend fun clearDataStore() {
+        applicationContext.dataStore.edit { it.clear() }
+    }
+
     companion object {
-        const val DATASTORE_NAME = "TutorialScheduler"
         const val IS_LAUNCHED_SUFFIX = "_IS_LAUNCHED"
         const val CONNECT_TIME_SUFFIX = "_CONNECTED_TIME"
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java
index f178708..04604e0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java
@@ -16,8 +16,6 @@
 
 package com.android.systemui.statusbar.phone;
 
-
-
 import android.annotation.Nullable;
 import android.content.Context;
 import android.content.res.Configuration;
@@ -38,11 +36,8 @@
 import com.android.systemui.Dependency;
 import com.android.systemui.Flags;
 import com.android.systemui.Gefingerpoken;
-import com.android.systemui.plugins.DarkIconDispatcher;
-import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver;
 import com.android.systemui.res.R;
 import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherContainer;
-import com.android.systemui.statusbar.policy.Clock;
 import com.android.systemui.statusbar.window.StatusBarWindowController;
 import com.android.systemui.user.ui.binder.StatusBarUserChipViewBinder;
 import com.android.systemui.user.ui.viewmodel.StatusBarUserChipViewModel;
@@ -55,8 +50,6 @@
     private final StatusBarContentInsetsProvider mContentInsetsProvider;
     private final StatusBarWindowController mStatusBarWindowController;
 
-    private DarkReceiver mBattery;
-    private Clock mClock;
     private int mRotationOrientation = -1;
     @Nullable
     private View mCutoutSpace;
@@ -93,8 +86,6 @@
     @Override
     public void onFinishInflate() {
         super.onFinishInflate();
-        mBattery = findViewById(R.id.battery);
-        mClock = findViewById(R.id.clock);
         mCutoutSpace = findViewById(R.id.cutout_space_view);
 
         updateResources();
@@ -103,9 +94,6 @@
     @Override
     protected void onAttachedToWindow() {
         super.onAttachedToWindow();
-        // Always have Battery meters in the status bar observe the dark/light modes.
-        Dependency.get(DarkIconDispatcher.class).addDarkReceiver(mBattery);
-        Dependency.get(DarkIconDispatcher.class).addDarkReceiver(mClock);
         if (updateDisplayParameters()) {
             updateLayoutForCutout();
             updateWindowHeight();
@@ -115,8 +103,6 @@
     @Override
     protected void onDetachedFromWindow() {
         super.onDetachedFromWindow();
-        Dependency.get(DarkIconDispatcher.class).removeDarkReceiver(mBattery);
-        Dependency.get(DarkIconDispatcher.class).removeDarkReceiver(mClock);
         mDisplayCutout = null;
     }
 
@@ -136,10 +122,6 @@
         updateWindowHeight();
     }
 
-    void onDensityOrFontScaleChanged() {
-        mClock.onDensityOrFontScaleChanged();
-    }
-
     @Override
     public WindowInsets onApplyWindowInsets(WindowInsets insets) {
         if (updateDisplayParameters()) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt
index a818c05..468a3c3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt
@@ -23,10 +23,13 @@
 import android.view.View
 import android.view.ViewGroup
 import android.view.ViewTreeObserver
+import androidx.annotation.VisibleForTesting
 import com.android.systemui.Flags
 import com.android.systemui.Gefingerpoken
+import com.android.systemui.battery.BatteryMeterView
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.flags.Flags.ENABLE_UNFOLD_STATUS_BAR_ANIMATIONS
+import com.android.systemui.plugins.DarkIconDispatcher
 import com.android.systemui.res.R
 import com.android.systemui.scene.shared.flag.SceneContainerFlag
 import com.android.systemui.scene.ui.view.WindowRootView
@@ -35,6 +38,7 @@
 import com.android.systemui.shade.ShadeViewController
 import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor
 import com.android.systemui.shared.animation.UnfoldMoveFromCenterAnimator
+import com.android.systemui.statusbar.policy.Clock
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.statusbar.window.StatusBarWindowStateController
 import com.android.systemui.unfold.SysUIUnfoldComponent
@@ -68,19 +72,27 @@
     private val viewUtil: ViewUtil,
     private val configurationController: ConfigurationController,
     private val statusOverlayHoverListenerFactory: StatusOverlayHoverListenerFactory,
+    private val darkIconDispatcher: DarkIconDispatcher,
 ) : ViewController<PhoneStatusBarView>(view) {
 
+    private lateinit var battery: BatteryMeterView
+    private lateinit var clock: Clock
     private lateinit var statusContainer: View
 
     private val configurationListener =
         object : ConfigurationController.ConfigurationListener {
             override fun onDensityOrFontScaleChanged() {
-                mView.onDensityOrFontScaleChanged()
+                clock.onDensityOrFontScaleChanged()
             }
         }
 
     override fun onViewAttached() {
         statusContainer = mView.requireViewById(R.id.system_icons)
+        clock = mView.requireViewById(R.id.clock)
+        battery = mView.requireViewById(R.id.battery)
+
+        addDarkReceivers()
+
         statusContainer.setOnHoverListener(
             statusOverlayHoverListenerFactory.createDarkAwareListener(statusContainer)
         )
@@ -133,7 +145,9 @@
         }
     }
 
-    override fun onViewDetached() {
+    @VisibleForTesting
+    public override fun onViewDetached() {
+        removeDarkReceivers()
         statusContainer.setOnHoverListener(null)
         progressProvider?.setReadyToHandleTransition(false)
         moveFromCenterAnimationController?.onViewDetached()
@@ -182,6 +196,16 @@
         }
     }
 
+    private fun addDarkReceivers() {
+        darkIconDispatcher.addDarkReceiver(battery)
+        darkIconDispatcher.addDarkReceiver(clock)
+    }
+
+    private fun removeDarkReceivers() {
+        darkIconDispatcher.removeDarkReceiver(battery)
+        darkIconDispatcher.removeDarkReceiver(clock)
+    }
+
     inner class PhoneStatusBarViewTouchHandler : Gefingerpoken {
         override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
             return if (Flags.statusBarSwipeOverChip()) {
@@ -285,6 +309,7 @@
         private val viewUtil: ViewUtil,
         private val configurationController: ConfigurationController,
         private val statusOverlayHoverListenerFactory: StatusOverlayHoverListenerFactory,
+        private val darkIconDispatcher: DarkIconDispatcher,
     ) {
         fun create(view: PhoneStatusBarView): PhoneStatusBarViewController {
             val statusBarMoveFromCenterAnimationController =
@@ -309,6 +334,7 @@
                 viewUtil,
                 configurationController,
                 statusOverlayHoverListenerFactory,
+                darkIconDispatcher,
             )
         }
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerContentTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerContentTest.kt
index a4936e6..8e215f9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerContentTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerContentTest.kt
@@ -33,9 +33,10 @@
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.bouncer.ui.BouncerDialogFactory
 import com.android.systemui.bouncer.ui.helper.BouncerSceneLayout
-import com.android.systemui.bouncer.ui.viewmodel.bouncerViewModel
+import com.android.systemui.bouncer.ui.viewmodel.bouncerSceneContentViewModelFactory
 import com.android.systemui.flags.Flags
 import com.android.systemui.flags.fakeFeatureFlagsClassic
+import com.android.systemui.lifecycle.rememberViewModel
 import com.android.systemui.motion.createSysUiComposeMotionTestRule
 import com.android.systemui.scene.domain.startable.sceneContainerStartable
 import com.android.systemui.testKosmos
@@ -81,7 +82,8 @@
     private fun BouncerContentUnderTest() {
         PlatformTheme {
             BouncerContent(
-                viewModel = kosmos.bouncerViewModel,
+                viewModel =
+                    rememberViewModel { kosmos.bouncerSceneContentViewModelFactory.create() },
                 layout = BouncerSceneLayout.BESIDE_USER_SWITCHER,
                 modifier = Modifier.fillMaxSize().testTag("BouncerContent"),
                 dialogFactory = bouncerDialogFactory
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/PatternBouncerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/PatternBouncerTest.kt
index 2948c02..4b61a0d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/PatternBouncerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/PatternBouncerTest.kt
@@ -24,14 +24,14 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.bouncer.domain.interactor.bouncerInteractor
-import com.android.systemui.bouncer.ui.viewmodel.PatternBouncerViewModel
-import com.android.systemui.kosmos.testScope
+import com.android.systemui.bouncer.ui.viewmodel.patternBouncerViewModelFactory
+import com.android.systemui.lifecycle.activateIn
 import com.android.systemui.motion.createSysUiComposeMotionTestRule
 import com.android.systemui.testKosmos
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.takeWhile
+import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -51,15 +51,15 @@
 
     @get:Rule val motionTestRule = createSysUiComposeMotionTestRule(kosmos)
 
-    private val bouncerInteractor by lazy { kosmos.bouncerInteractor }
-    private val viewModel by lazy {
-        PatternBouncerViewModel(
-            applicationContext = context,
-            viewModelScope = kosmos.testScope.backgroundScope,
-            interactor = bouncerInteractor,
+    private val viewModel =
+        kosmos.patternBouncerViewModelFactory.create(
             isInputEnabled = MutableStateFlow(true).asStateFlow(),
             onIntentionalUserInput = {},
         )
+
+    @Before
+    fun setUp() {
+        viewModel.activateIn(motionTestRule.toolkit.testScope)
     }
 
     @Composable
diff --git a/packages/SystemUI/tests/src/com/android/systemui/inputdevice/data/repository/TutorialSchedulerRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/inputdevice/data/repository/TutorialSchedulerRepositoryTest.kt
new file mode 100644
index 0000000..7583399
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/inputdevice/data/repository/TutorialSchedulerRepositoryTest.kt
@@ -0,0 +1,87 @@
+/*
+ * 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.inputdevice.data.repository
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.inputdevice.tutorial.data.repository.DeviceType.KEYBOARD
+import com.android.systemui.inputdevice.tutorial.data.repository.DeviceType.TOUCHPAD
+import com.android.systemui.inputdevice.tutorial.data.repository.TutorialSchedulerRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
+import com.google.common.truth.Truth.assertThat
+import java.time.Instant
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.runTest
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class TutorialSchedulerRepositoryTest : SysuiTestCase() {
+
+    private lateinit var underTest: TutorialSchedulerRepository
+    private val kosmos = Kosmos()
+    private val testScope = kosmos.testScope
+
+    @Before
+    fun setup() {
+        underTest =
+            TutorialSchedulerRepository(
+                context,
+                testScope.backgroundScope,
+                "TutorialSchedulerRepositoryTest"
+            )
+    }
+
+    @After
+    fun clear() {
+        testScope.launch { underTest.clearDataStore() }
+    }
+
+    @Test
+    fun initialState() =
+        testScope.runTest {
+            assertThat(underTest.wasEverConnected(KEYBOARD)).isFalse()
+            assertThat(underTest.wasEverConnected(TOUCHPAD)).isFalse()
+            assertThat(underTest.isLaunched(KEYBOARD)).isFalse()
+            assertThat(underTest.isLaunched(TOUCHPAD)).isFalse()
+        }
+
+    @Test
+    fun connectKeyboard() =
+        testScope.runTest {
+            val now = Instant.now().toEpochMilli()
+            underTest.updateConnectTime(KEYBOARD, now)
+
+            assertThat(underTest.wasEverConnected(KEYBOARD)).isTrue()
+            assertThat(underTest.connectTime(KEYBOARD)).isEqualTo(now)
+            assertThat(underTest.wasEverConnected(TOUCHPAD)).isFalse()
+        }
+
+    @Test
+    fun launchKeyboard() =
+        testScope.runTest {
+            underTest.updateLaunch(KEYBOARD)
+
+            assertThat(underTest.isLaunched(KEYBOARD)).isTrue()
+            assertThat(underTest.isLaunched(TOUCHPAD)).isFalse()
+        }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java
index 2d11917..63192f3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java
@@ -36,6 +36,7 @@
 import android.animation.Animator;
 import android.animation.ValueAnimator.AnimatorUpdateListener;
 import android.os.Handler;
+import android.platform.test.annotations.EnableFlags;
 import android.service.notification.StatusBarNotification;
 import android.testing.TestableLooper;
 import android.view.MotionEvent;
@@ -52,6 +53,7 @@
 import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
 import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper.SnoozeOption;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
+import com.android.systemui.statusbar.notification.shared.NotificationContentAlphaOptimization;
 
 import org.junit.Before;
 import org.junit.Rule;
@@ -672,17 +674,31 @@
     }
 
     @Test
-    public void testForceResetSwipeStateDoesNothingIfTranslationIsZero() {
+    public void testForceResetSwipeStateDoesNothingIfTranslationIsZeroAndAlphaIsOne() {
         doReturn(FAKE_ROW_WIDTH).when(mNotificationRow).getMeasuredWidth();
         doReturn(0f).when(mNotificationRow).getTranslationX();
+        doReturn(1f).when(mNotificationRow).getAlpha();
 
         mSwipeHelper.forceResetSwipeState(mNotificationRow);
 
         verify(mNotificationRow).getTranslationX();
+        verify(mNotificationRow).getAlpha();
         verifyNoMoreInteractions(mNotificationRow);
     }
 
     @Test
+    @EnableFlags(NotificationContentAlphaOptimization.FLAG_NAME)
+    public void testForceResetSwipeStateResetsAlphaIfTranslationIsZeroAndAlphaNotOne() {
+        doReturn(FAKE_ROW_WIDTH).when(mNotificationRow).getMeasuredWidth();
+        doReturn(0f).when(mNotificationRow).getTranslationX();
+        doReturn(0.5f).when(mNotificationRow).getAlpha();
+
+        mSwipeHelper.forceResetSwipeState(mNotificationRow);
+
+        verify(mNotificationRow).setContentAlpha(eq(1f));
+    }
+
+    @Test
     public void testForceResetSwipeStateResetsTranslationAndAlpha() {
         doReturn(FAKE_ROW_WIDTH).when(mNotificationRow).getMeasuredWidth();
         doReturn(10f).when(mNotificationRow).getTranslationX();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt
index 5b45781..30e7247 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt
@@ -33,8 +33,11 @@
 import androidx.test.filters.SmallTest
 import androidx.test.platform.app.InstrumentationRegistry
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.battery.BatteryMeterView
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.flags.Flags
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.plugins.fakeDarkIconDispatcher
 import com.android.systemui.res.R
 import com.android.systemui.scene.ui.view.WindowRootView
 import com.android.systemui.shade.ShadeControllerImpl
@@ -42,6 +45,7 @@
 import com.android.systemui.shade.ShadeViewController
 import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor
 import com.android.systemui.statusbar.CommandQueue
+import com.android.systemui.statusbar.policy.Clock
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.statusbar.window.StatusBarWindowStateController
 import com.android.systemui.unfold.SysUIUnfoldComponent
@@ -70,7 +74,9 @@
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class PhoneStatusBarViewControllerTest : SysuiTestCase() {
+    private val kosmos = Kosmos()
 
+    private val fakeDarkIconDispatcher = kosmos.fakeDarkIconDispatcher
     @Mock private lateinit var shadeViewController: ShadeViewController
     @Mock private lateinit var panelExpansionInteractor: PanelExpansionInteractor
     @Mock private lateinit var featureFlags: FeatureFlags
@@ -91,6 +97,12 @@
     private lateinit var view: PhoneStatusBarView
     private lateinit var controller: PhoneStatusBarViewController
 
+    private val clockView: Clock
+        get() = view.requireViewById(R.id.clock)
+
+    private val batteryView: BatteryMeterView
+        get() = view.requireViewById(R.id.battery)
+
     private val unfoldConfig = UnfoldConfig()
 
     @Before
@@ -114,16 +126,25 @@
     @Test
     fun onViewAttachedAndDrawn_startListeningConfigurationControllerCallback() {
         val view = createViewMock()
-        val argumentCaptor =
-            ArgumentCaptor.forClass(ConfigurationController.ConfigurationListener::class.java)
+
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
             controller = createAndInitController(view)
         }
 
-        verify(configurationController).addCallback(argumentCaptor.capture())
-        argumentCaptor.value.onDensityOrFontScaleChanged()
+        verify(configurationController).addCallback(any())
+    }
 
-        verify(view).onDensityOrFontScaleChanged()
+    @Test
+    fun onViewAttachedAndDrawn_darkReceiversRegistered() {
+        val view = createViewMock()
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            controller = createAndInitController(view)
+        }
+
+        assertThat(fakeDarkIconDispatcher.receivers.size).isEqualTo(2)
+        assertThat(fakeDarkIconDispatcher.receivers).contains(clockView)
+        assertThat(fakeDarkIconDispatcher.receivers).contains(batteryView)
     }
 
     @Test
@@ -158,6 +179,21 @@
     }
 
     @Test
+    fun onViewDetached_darkReceiversUnregistered() {
+        val view = createViewMock()
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            controller = createAndInitController(view)
+        }
+
+        assertThat(fakeDarkIconDispatcher.receivers).isNotEmpty()
+
+        controller.onViewDetached()
+
+        assertThat(fakeDarkIconDispatcher.receivers).isEmpty()
+    }
+
+    @Test
     fun handleTouchEventFromStatusBar_panelsNotEnabled_returnsFalseAndNoViewEvent() {
         `when`(centralSurfacesImpl.commandQueuePanelsEnabled).thenReturn(false)
         val returnVal =
@@ -353,7 +389,8 @@
                 shadeLogger,
                 viewUtil,
                 configurationController,
-                mStatusOverlayHoverListenerFactory
+                mStatusOverlayHoverListenerFactory,
+                fakeDarkIconDispatcher,
             )
             .create(view)
             .also { it.init() }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewTest.kt
index abc50bc..ed5ec7b2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewTest.kt
@@ -35,7 +35,6 @@
 import com.android.systemui.Flags.FLAG_STATUS_BAR_SWIPE_OVER_CHIP
 import com.android.systemui.Gefingerpoken
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.plugins.DarkIconDispatcher
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.window.StatusBarWindowController
 import com.android.systemui.util.mockito.mock
@@ -64,7 +63,6 @@
             StatusBarContentInsetsProvider::class.java,
             contentInsetsProvider
         )
-        mDependency.injectTestDependency(DarkIconDispatcher::class.java, mock<DarkIconDispatcher>())
         mDependency.injectTestDependency(StatusBarWindowController::class.java, windowController)
         context.ensureTestableResources()
         view = spy(createStatusBarView())
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelKosmos.kt
index e70631e..e8612d08 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelKosmos.kt
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
 package com.android.systemui.bouncer.ui.viewmodel
 
 import android.content.applicationContext
@@ -26,26 +28,31 @@
 import com.android.systemui.deviceentry.domain.interactor.deviceEntryFaceAuthInteractor
 import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor
 import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.kosmos.testScope
+import com.android.systemui.kosmos.Kosmos.Fixture
 import com.android.systemui.user.ui.viewmodel.userSwitcherViewModel
 import com.android.systemui.util.time.systemClock
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 
-@ExperimentalCoroutinesApi
-val Kosmos.bouncerMessageViewModel by
-    Kosmos.Fixture {
-        BouncerMessageViewModel(
-            applicationContext = applicationContext,
-            applicationScope = testScope.backgroundScope,
-            bouncerInteractor = bouncerInteractor,
-            simBouncerInteractor = simBouncerInteractor,
-            authenticationInteractor = authenticationInteractor,
-            selectedUser = userSwitcherViewModel.selectedUser,
-            clock = systemClock,
-            biometricMessageInteractor = biometricMessageInteractor,
-            faceAuthInteractor = deviceEntryFaceAuthInteractor,
-            deviceUnlockedInteractor = deviceUnlockedInteractor,
-            deviceEntryBiometricsAllowedInteractor = deviceEntryBiometricsAllowedInteractor,
-            flags = composeBouncerFlags,
-        )
+val Kosmos.bouncerMessageViewModel by Fixture {
+    BouncerMessageViewModel(
+        applicationContext = applicationContext,
+        bouncerInteractor = bouncerInteractor,
+        simBouncerInteractor = simBouncerInteractor,
+        authenticationInteractor = authenticationInteractor,
+        userSwitcherViewModel = userSwitcherViewModel,
+        clock = systemClock,
+        biometricMessageInteractor = biometricMessageInteractor,
+        faceAuthInteractor = deviceEntryFaceAuthInteractor,
+        deviceUnlockedInteractor = deviceUnlockedInteractor,
+        deviceEntryBiometricsAllowedInteractor = deviceEntryBiometricsAllowedInteractor,
+        flags = composeBouncerFlags,
+    )
+}
+
+val Kosmos.bouncerMessageViewModelFactory by Fixture {
+    object : BouncerMessageViewModel.Factory {
+        override fun create(): BouncerMessageViewModel {
+            return bouncerMessageViewModel
+        }
     }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt
index c3dad74..e405d17 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt
@@ -21,6 +21,7 @@
 import android.app.admin.devicePolicyManager
 import android.content.applicationContext
 import com.android.systemui.authentication.domain.interactor.authenticationInteractor
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.bouncer.domain.interactor.bouncerActionButtonInteractor
 import com.android.systemui.bouncer.domain.interactor.bouncerInteractor
 import com.android.systemui.bouncer.domain.interactor.simBouncerInteractor
@@ -28,28 +29,97 @@
 import com.android.systemui.inputmethod.domain.interactor.inputMethodInteractor
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
-import com.android.systemui.kosmos.testDispatcher
-import com.android.systemui.kosmos.testScope
 import com.android.systemui.user.domain.interactor.selectedUserInteractor
 import com.android.systemui.user.ui.viewmodel.userSwitcherViewModel
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.StateFlow
 
-val Kosmos.bouncerViewModel by Fixture {
-    BouncerViewModel(
-        applicationContext = applicationContext,
-        applicationScope = testScope.backgroundScope,
-        mainDispatcher = testDispatcher,
+val Kosmos.bouncerSceneActionsViewModel by Fixture {
+    BouncerSceneActionsViewModel(
         bouncerInteractor = bouncerInteractor,
-        inputMethodInteractor = inputMethodInteractor,
-        simBouncerInteractor = simBouncerInteractor,
-        authenticationInteractor = authenticationInteractor,
-        selectedUserInteractor = selectedUserInteractor,
-        devicePolicyManager = devicePolicyManager,
-        bouncerMessageViewModel = bouncerMessageViewModel,
-        flags = composeBouncerFlags,
-        selectedUser = userSwitcherViewModel.selectedUser,
-        users = userSwitcherViewModel.users,
-        userSwitcherMenu = userSwitcherViewModel.menu,
-        actionButton = bouncerActionButtonInteractor.actionButton,
     )
 }
+
+val Kosmos.bouncerSceneActionsViewModelFactory by Fixture {
+    object : BouncerSceneActionsViewModel.Factory {
+        override fun create(): BouncerSceneActionsViewModel {
+            return bouncerSceneActionsViewModel
+        }
+    }
+}
+
+val Kosmos.bouncerSceneContentViewModel by Fixture {
+    BouncerSceneContentViewModel(
+        applicationContext = applicationContext,
+        bouncerInteractor = bouncerInteractor,
+        authenticationInteractor = authenticationInteractor,
+        devicePolicyManager = devicePolicyManager,
+        bouncerMessageViewModelFactory = bouncerMessageViewModelFactory,
+        flags = composeBouncerFlags,
+        userSwitcher = userSwitcherViewModel,
+        actionButtonInteractor = bouncerActionButtonInteractor,
+        pinViewModelFactory = pinBouncerViewModelFactory,
+        patternViewModelFactory = patternBouncerViewModelFactory,
+        passwordViewModelFactory = passwordBouncerViewModelFactory,
+    )
+}
+
+val Kosmos.bouncerSceneContentViewModelFactory by Fixture {
+    object : BouncerSceneContentViewModel.Factory {
+        override fun create(): BouncerSceneContentViewModel {
+            return bouncerSceneContentViewModel
+        }
+    }
+}
+
+val Kosmos.pinBouncerViewModelFactory by Fixture {
+    object : PinBouncerViewModel.Factory {
+        override fun create(
+            isInputEnabled: StateFlow<Boolean>,
+            onIntentionalUserInput: () -> Unit,
+            authenticationMethod: AuthenticationMethodModel,
+        ): PinBouncerViewModel {
+            return PinBouncerViewModel(
+                applicationContext = applicationContext,
+                interactor = bouncerInteractor,
+                simBouncerInteractor = simBouncerInteractor,
+                isInputEnabled = isInputEnabled,
+                onIntentionalUserInput = onIntentionalUserInput,
+                authenticationMethod = authenticationMethod,
+            )
+        }
+    }
+}
+
+val Kosmos.patternBouncerViewModelFactory by Fixture {
+    object : PatternBouncerViewModel.Factory {
+        override fun create(
+            isInputEnabled: StateFlow<Boolean>,
+            onIntentionalUserInput: () -> Unit,
+        ): PatternBouncerViewModel {
+            return PatternBouncerViewModel(
+                applicationContext = applicationContext,
+                interactor = bouncerInteractor,
+                isInputEnabled = isInputEnabled,
+                onIntentionalUserInput = onIntentionalUserInput,
+            )
+        }
+    }
+}
+
+val Kosmos.passwordBouncerViewModelFactory by Fixture {
+    object : PasswordBouncerViewModel.Factory {
+        override fun create(
+            isInputEnabled: StateFlow<Boolean>,
+            onIntentionalUserInput: () -> Unit,
+        ): PasswordBouncerViewModel {
+            return PasswordBouncerViewModel(
+                interactor = bouncerInteractor,
+                inputMethodInteractor = inputMethodInteractor,
+                selectedUserInteractor = selectedUserInteractor,
+                isInputEnabled = isInputEnabled,
+                onIntentionalUserInput = onIntentionalUserInput,
+            )
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/WidgetTrampolineInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/WidgetTrampolineInteractorKosmos.kt
new file mode 100644
index 0000000..8124224
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/WidgetTrampolineInteractorKosmos.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.communal.domain.interactor
+
+import com.android.systemui.common.usagestats.domain.interactor.usageStatsInteractor
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.log.logcatLogBuffer
+import com.android.systemui.plugins.activityStarter
+import com.android.systemui.shared.system.taskStackChangeListeners
+import com.android.systemui.util.time.fakeSystemClock
+
+val Kosmos.widgetTrampolineInteractor: WidgetTrampolineInteractor by
+    Kosmos.Fixture {
+        WidgetTrampolineInteractor(
+            activityStarter = activityStarter,
+            systemClock = fakeSystemClock,
+            keyguardTransitionInteractor = keyguardTransitionInteractor,
+            taskStackChangeListeners = taskStackChangeListeners,
+            usageStatsInteractor = usageStatsInteractor,
+            logBuffer = logcatLogBuffer("WidgetTrampolineInteractor"),
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/DarkIconDispatcherKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/DarkIconDispatcherKosmos.kt
new file mode 100644
index 0000000..3d125e9
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/DarkIconDispatcherKosmos.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.plugins
+
+import com.android.systemui.kosmos.Kosmos
+
+var Kosmos.fakeDarkIconDispatcher: FakeDarkIconDispatcher by
+    Kosmos.Fixture { FakeDarkIconDispatcher() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/FakeDarkIconDispatcher.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/FakeDarkIconDispatcher.kt
new file mode 100644
index 0000000..102a853
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/FakeDarkIconDispatcher.kt
@@ -0,0 +1,36 @@
+/*
+ * 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.plugins
+
+import android.graphics.Rect
+import java.util.ArrayList
+
+class FakeDarkIconDispatcher : DarkIconDispatcher {
+    val receivers = mutableListOf<DarkIconDispatcher.DarkReceiver>()
+
+    override fun setIconsDarkArea(r: ArrayList<Rect>) {}
+
+    override fun addDarkReceiver(receiver: DarkIconDispatcher.DarkReceiver) {
+        receivers.add(receiver)
+    }
+
+    override fun removeDarkReceiver(receiver: DarkIconDispatcher.DarkReceiver) {
+        receivers.remove(receiver)
+    }
+
+    override fun applyDark(`object`: DarkIconDispatcher.DarkReceiver) {}
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shared/system/TaskStackChangeListenersKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shared/system/TaskStackChangeListenersKosmos.kt
new file mode 100644
index 0000000..67f611a
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shared/system/TaskStackChangeListenersKosmos.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shared.system
+
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.taskStackChangeListeners: TaskStackChangeListeners by
+    Kosmos.Fixture { TaskStackChangeListeners.getTestInstance() }
diff --git a/services/contentcapture/java/com/android/server/contentprotection/OWNERS b/services/contentcapture/java/com/android/server/contentprotection/OWNERS
new file mode 100644
index 0000000..3d09da3
--- /dev/null
+++ b/services/contentcapture/java/com/android/server/contentprotection/OWNERS
@@ -0,0 +1,4 @@
+# Bug component: 1040349
+
+include /core/java/android/view/contentprotection/OWNERS
+
diff --git a/services/core/java/com/android/server/notification/NotificationAttentionHelper.java b/services/core/java/com/android/server/notification/NotificationAttentionHelper.java
index 9818916..abb2132 100644
--- a/services/core/java/com/android/server/notification/NotificationAttentionHelper.java
+++ b/services/core/java/com/android/server/notification/NotificationAttentionHelper.java
@@ -489,6 +489,7 @@
 
                 } else if ((record.getFlags() & Notification.FLAG_INSISTENT) != 0) {
                     hasValidSound = false;
+                    hasValidVibrate = false;
                 }
             }
         }
@@ -753,6 +754,13 @@
         // notifying app does not have the VIBRATE permission.
         final long identity = Binder.clearCallingIdentity();
         try {
+            // Need to explicitly cancel a previously playing vibration
+            // Otherwise a looping vibration will not be stopped when starting a new one.
+            if (mVibrateNotificationKey != null
+                    && !mVibrateNotificationKey.equals(record.getKey())) {
+                mVibrateNotificationKey = null;
+                mVibratorHelper.cancelVibration();
+            }
             final float scale = getVibrationIntensity(record);
             final VibrationEffect scaledEffect = Float.compare(scale, DEFAULT_VOLUME) != 0
                     ? mVibratorHelper.scale(effect, scale) : effect;
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index 5bb4a8a..121ab2c 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -3771,6 +3771,7 @@
             // Shell calls back into Core with the entry bounds to be applied with startWCT.
             final Transition enterPipTransition = new Transition(TRANSIT_PIP,
                     0 /* flags */, getTransitionController(), mWindowManager.mSyncEngine);
+            r.setPictureInPictureParams(params);
             enterPipTransition.setPipActivity(r);
             r.mAutoEnteringPip = isAutoEnter;
             getTransitionController().startCollectOrQueue(enterPipTransition, (deferred) -> {
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 3f0c9fd..4340771 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -4705,6 +4705,15 @@
                 // it does not follow the ActivityStarter path.
                 if (topActivity.shouldBeVisible()) {
                     mAtmService.resumeAppSwitches();
+                    // In pip1, when expanding pip to full-screen, the "behind" task is not
+                    // actually becoming invisible since task windowing mode is pinned.
+                    if (!isPip2ExperimentEnabled) {
+                        final ActivityRecord ar = mAtmService.mLastResumedActivity;
+                        if (ar != null && ar.getTask() != null) {
+                            mAtmService.takeTaskSnapshot(ar.getTask().mTaskId,
+                                    true /* updateCache */);
+                        }
+                    }
                 }
             } else if (isPip2ExperimentEnabled) {
                 super.setWindowingMode(windowingMode);
diff --git a/services/tests/servicestests/src/com/android/server/contentprotection/OWNERS b/services/tests/servicestests/src/com/android/server/contentprotection/OWNERS
index 24561c5..3d09da3 100644
--- a/services/tests/servicestests/src/com/android/server/contentprotection/OWNERS
+++ b/services/tests/servicestests/src/com/android/server/contentprotection/OWNERS
@@ -1,3 +1,4 @@
-# Bug component: 544200
+# Bug component: 1040349
 
-include /core/java/android/view/contentcapture/OWNERS
+include /core/java/android/view/contentprotection/OWNERS
+
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java
index 643ee4a..62e5b9a 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java
@@ -2007,6 +2007,25 @@
     }
 
     @Test
+    public void testCanInterruptNonRingtoneInsistentBuzzWithOtherBuzzyNotification() {
+        NotificationRecord r = getInsistentBuzzyNotification();
+        mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+        verifyVibrateLooped();
+        assertTrue(r.isInterruptive());
+        assertNotEquals(-1, r.getLastAudiblyAlertedMs());
+        Mockito.reset(mVibrator);
+
+        // New buzzy notification stops previous looping vibration
+        NotificationRecord interrupter = getBuzzyOtherNotification();
+        mAttentionHelper.buzzBeepBlinkLocked(interrupter, DEFAULT_SIGNALS);
+        verifyStopVibrate();
+        // And then vibrates itself
+        verifyVibrate(1);
+        assertTrue(interrupter.isInterruptive());
+        assertNotEquals(-1, interrupter.getLastAudiblyAlertedMs());
+    }
+
+    @Test
     public void testRingtoneInsistentBeep_doesNotBlockFutureSoundsOnceStopped() throws Exception {
         NotificationChannel ringtoneChannel =
             new NotificationChannel("ringtone", "", IMPORTANCE_HIGH);
diff --git a/telephony/java/android/telephony/satellite/ISatelliteProvisionStateCallback.aidl b/telephony/java/android/telephony/satellite/ISatelliteProvisionStateCallback.aidl
index f981fb1..5f0d986 100644
--- a/telephony/java/android/telephony/satellite/ISatelliteProvisionStateCallback.aidl
+++ b/telephony/java/android/telephony/satellite/ISatelliteProvisionStateCallback.aidl
@@ -16,6 +16,8 @@
 
 package android.telephony.satellite;
 
+import android.telephony.satellite.SatelliteSubscriberProvisionStatus;
+
 /**
  * Interface for satellite provision state callback.
  * @hide
@@ -27,4 +29,14 @@
      * @param provisioned True means the service is provisioned and false means it is not.
      */
     void onSatelliteProvisionStateChanged(in boolean provisioned);
+
+    /**
+     * Called when the provisioning state of one or more SatelliteSubscriberInfos changes.
+     *
+     * @param satelliteSubscriberProvisionStatus The List contains the latest provisioning states of
+     * the SatelliteSubscriberInfos.
+     * @hide
+     */
+    void onSatelliteSubscriptionProvisionStateChanged(in List<SatelliteSubscriberProvisionStatus>
+        satelliteSubscriberProvisionStatus);
 }
diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java
index e657d7f..0c98327 100644
--- a/telephony/java/android/telephony/satellite/SatelliteManager.java
+++ b/telephony/java/android/telephony/satellite/SatelliteManager.java
@@ -1404,6 +1404,16 @@
                                         () -> callback.onSatelliteProvisionStateChanged(
                                                 provisioned)));
                             }
+
+                            @FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
+                            @Override
+                            public void onSatelliteSubscriptionProvisionStateChanged(
+                                    @NonNull List<SatelliteSubscriberProvisionStatus>
+                                            satelliteSubscriberProvisionStatus) {
+                                executor.execute(() -> Binder.withCleanCallingIdentity(() ->
+                                        callback.onSatelliteSubscriptionProvisionStateChanged(
+                                                satelliteSubscriberProvisionStatus)));
+                            }
                         };
                 sSatelliteProvisionStateCallbackMap.put(callback, internalCallback);
                 return telephony.registerForSatelliteProvisionStateChanged(
diff --git a/telephony/java/android/telephony/satellite/SatelliteProvisionStateCallback.java b/telephony/java/android/telephony/satellite/SatelliteProvisionStateCallback.java
index a12952b..e8ae0f5 100644
--- a/telephony/java/android/telephony/satellite/SatelliteProvisionStateCallback.java
+++ b/telephony/java/android/telephony/satellite/SatelliteProvisionStateCallback.java
@@ -17,10 +17,13 @@
 package android.telephony.satellite;
 
 import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
 import android.annotation.SystemApi;
 
 import com.android.internal.telephony.flags.Flags;
 
+import java.util.List;
+
 /**
  * A callback class for monitoring satellite provision state change events.
  *
@@ -39,4 +42,16 @@
      */
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     void onSatelliteProvisionStateChanged(boolean provisioned);
+
+    /**
+     * Called when the provisioning state of one or more SatelliteSubscriberInfos changes.
+     *
+     * @param satelliteSubscriberProvisionStatus The List contains the latest provisioning states
+     *                                           of the SatelliteSubscriberInfos.
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
+    default void onSatelliteSubscriptionProvisionStateChanged(
+            @NonNull List<SatelliteSubscriberProvisionStatus>
+                    satelliteSubscriberProvisionStatus) {};
 }
diff --git a/telephony/java/android/telephony/satellite/SatelliteSubscriberProvisionStatus.aidl b/telephony/java/android/telephony/satellite/SatelliteSubscriberProvisionStatus.aidl
new file mode 100644
index 0000000..80de779
--- /dev/null
+++ b/telephony/java/android/telephony/satellite/SatelliteSubscriberProvisionStatus.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2024, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.telephony.satellite;
+
+parcelable SatelliteSubscriberProvisionStatus;
\ No newline at end of file
diff --git a/telephony/java/android/telephony/satellite/SatelliteSubscriberProvisionStatus.java b/telephony/java/android/telephony/satellite/SatelliteSubscriberProvisionStatus.java
new file mode 100644
index 0000000..e3d619e
--- /dev/null
+++ b/telephony/java/android/telephony/satellite/SatelliteSubscriberProvisionStatus.java
@@ -0,0 +1,174 @@
+/*
+ * 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.telephony.satellite;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.telephony.flags.Flags;
+
+import java.util.Objects;
+
+/**
+ * Represents the provisioning state of SatelliteSubscriberInfo.
+ *
+ * @hide
+ */
+@FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
+public class SatelliteSubscriberProvisionStatus implements Parcelable {
+    private SatelliteSubscriberInfo mSubscriberInfo;
+    /** {@code true} mean the satellite subscriber is provisioned, {@code false} otherwise. */
+    private boolean mProvisionStatus;
+
+    public SatelliteSubscriberProvisionStatus(@NonNull Builder builder) {
+        mSubscriberInfo = builder.mSubscriberInfo;
+        mProvisionStatus = builder.mProvisionStatus;
+    }
+
+    /**
+     * Builder class for constructing SatelliteSubscriberProvisionStatus objects
+     *
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
+    public static class Builder {
+        private SatelliteSubscriberInfo mSubscriberInfo;
+        private boolean mProvisionStatus;
+
+        /**
+         * Set the SatelliteSubscriberInfo and returns the Builder class.
+         * @hide
+         */
+        public Builder setSatelliteSubscriberInfo(SatelliteSubscriberInfo satelliteSubscriberInfo) {
+            mSubscriberInfo = satelliteSubscriberInfo;
+            return this;
+        }
+
+        /**
+         * Set the SatelliteSubscriberInfo's provisionStatus and returns the Builder class.
+         * @hide
+         */
+        @NonNull
+        public Builder setProvisionStatus(boolean provisionStatus) {
+            mProvisionStatus = provisionStatus;
+            return this;
+        }
+
+        /**
+         * Returns SatelliteSubscriberProvisionStatus object.
+         * @hide
+         */
+        @NonNull
+        public SatelliteSubscriberProvisionStatus build() {
+            return new SatelliteSubscriberProvisionStatus(this);
+        }
+    }
+
+    private SatelliteSubscriberProvisionStatus(Parcel in) {
+        readFromParcel(in);
+    }
+
+    /**
+     * @hide
+     */
+    @Override
+    @FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
+    public void writeToParcel(@NonNull Parcel out, int flags) {
+        mSubscriberInfo.writeToParcel(out, flags);
+        out.writeBoolean(mProvisionStatus);
+    }
+
+    @FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
+    public static final @android.annotation.NonNull Creator<SatelliteSubscriberProvisionStatus>
+            CREATOR =
+            new Creator<SatelliteSubscriberProvisionStatus>() {
+                @Override
+                public SatelliteSubscriberProvisionStatus createFromParcel(Parcel in) {
+                    return new SatelliteSubscriberProvisionStatus(in);
+                }
+
+                @Override
+                public SatelliteSubscriberProvisionStatus[] newArray(int size) {
+                    return new SatelliteSubscriberProvisionStatus[size];
+                }
+            };
+
+    /**
+     * @hide
+     */
+    @Override
+    @FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
+    public int describeContents() {
+        return 0;
+    }
+
+    /**
+     * SatelliteSubscriberInfo that has a provisioning state.
+     * @return SatelliteSubscriberInfo.
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
+    public @NonNull SatelliteSubscriberInfo getSatelliteSubscriberInfo() {
+        return mSubscriberInfo;
+    }
+
+    /**
+     * SatelliteSubscriberInfo's provisioning state.
+     * @return {@code true} means provisioning. {@code false} means deprovisioning.
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
+    public @NonNull  boolean getProvisionStatus() {
+        return mProvisionStatus;
+    }
+
+    @NonNull
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+
+        sb.append("SatelliteSubscriberInfo:");
+        sb.append(mSubscriberInfo);
+        sb.append(",");
+
+        sb.append("ProvisionStatus:");
+        sb.append(mProvisionStatus);
+        return sb.toString();
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mSubscriberInfo, mProvisionStatus);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof SatelliteSubscriberProvisionStatus)) return false;
+        SatelliteSubscriberProvisionStatus that = (SatelliteSubscriberProvisionStatus) o;
+        return Objects.equals(mSubscriberInfo, that.mSubscriberInfo)
+                && mProvisionStatus == that.mProvisionStatus;
+    }
+
+    private void readFromParcel(Parcel in) {
+        mSubscriberInfo = in.readParcelable(SatelliteSubscriberInfo.class.getClassLoader(),
+                SatelliteSubscriberInfo.class);
+        mProvisionStatus = in.readBoolean();
+    }
+}
diff --git a/tests/Internal/src/com/android/internal/protolog/LegacyProtoLogImplTest.java b/tests/Internal/src/com/android/internal/protolog/LegacyProtoLogImplTest.java
index 5a48327..9657225 100644
--- a/tests/Internal/src/com/android/internal/protolog/LegacyProtoLogImplTest.java
+++ b/tests/Internal/src/com/android/internal/protolog/LegacyProtoLogImplTest.java
@@ -214,6 +214,13 @@
         verify(mReader, never()).getViewerString(anyLong());
     }
 
+    @Test
+    public void loadViewerConfigOnLogcatGroupRegistration() {
+        TestProtoLogGroup.TEST_GROUP.setLogToLogcat(true);
+        mProtoLog.registerGroups(TestProtoLogGroup.TEST_GROUP);
+        verify(mReader).loadViewerConfig(any(), any());
+    }
+
     private static class ProtoLogData {
         Long mMessageHash = null;
         Long mElapsedTime = null;
diff --git a/tests/SurfaceControlViewHostTest/src/com/android/test/viewembed/SurfaceControlViewHostSyncTest.java b/tests/SurfaceControlViewHostTest/src/com/android/test/viewembed/SurfaceControlViewHostSyncTest.java
index 359eb35..5012c23 100644
--- a/tests/SurfaceControlViewHostTest/src/com/android/test/viewembed/SurfaceControlViewHostSyncTest.java
+++ b/tests/SurfaceControlViewHostTest/src/com/android/test/viewembed/SurfaceControlViewHostSyncTest.java
@@ -84,6 +84,7 @@
         content.addView(enableSyncButton,
                 new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
                         ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.RIGHT | Gravity.BOTTOM));
+        content.setFitsSystemWindows(true);
         setContentView(content);
 
         mSv.setZOrderOnTop(false);
diff --git a/tests/SurfaceControlViewHostTest/src/com/android/test/viewembed/SurfaceControlViewHostTest.java b/tests/SurfaceControlViewHostTest/src/com/android/test/viewembed/SurfaceControlViewHostTest.java
index 73e0163..4119ea2 100644
--- a/tests/SurfaceControlViewHostTest/src/com/android/test/viewembed/SurfaceControlViewHostTest.java
+++ b/tests/SurfaceControlViewHostTest/src/com/android/test/viewembed/SurfaceControlViewHostTest.java
@@ -37,6 +37,7 @@
 
     protected void onCreate(Bundle savedInstanceState) {
         FrameLayout content = new FrameLayout(this);
+        content.setFitsSystemWindows(true);
         super.onCreate(savedInstanceState);
         mView = new SurfaceView(this);
         content.addView(mView, new FrameLayout.LayoutParams(
diff --git a/tests/SurfaceControlViewHostTest/src/com/android/test/viewembed/SurfaceInputTestActivity.java b/tests/SurfaceControlViewHostTest/src/com/android/test/viewembed/SurfaceInputTestActivity.java
index ac7dc9e..5287068 100644
--- a/tests/SurfaceControlViewHostTest/src/com/android/test/viewembed/SurfaceInputTestActivity.java
+++ b/tests/SurfaceControlViewHostTest/src/com/android/test/viewembed/SurfaceInputTestActivity.java
@@ -88,6 +88,7 @@
     protected void onCreate(@Nullable Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         LinearLayout content = new LinearLayout(this);
+        content.setFitsSystemWindows(true);
         mLocalSurfaceView = new SurfaceView(this);
         content.addView(mLocalSurfaceView, new LinearLayout.LayoutParams(
                 500, 500, Gravity.CENTER_HORIZONTAL | Gravity.TOP));