Merge "Jank CUJ: Maximizing Non-Resizable App" into main
diff --git a/core/java/android/app/appfunctions/ExecuteAppFunctionRequest.java b/core/java/android/app/appfunctions/ExecuteAppFunctionRequest.java
index a50425e..db3de62 100644
--- a/core/java/android/app/appfunctions/ExecuteAppFunctionRequest.java
+++ b/core/java/android/app/appfunctions/ExecuteAppFunctionRequest.java
@@ -40,8 +40,8 @@
                 public ExecuteAppFunctionRequest createFromParcel(Parcel parcel) {
                     String targetPackageName = parcel.readString8();
                     String functionIdentifier = parcel.readString8();
-                    GenericDocument parameters;
-                    parameters = GenericDocument.createFromParcel(parcel);
+                    GenericDocumentWrapper parameters = GenericDocumentWrapper
+                            .CREATOR.createFromParcel(parcel);
                     Bundle extras = parcel.readBundle(Bundle.class.getClassLoader());
                     return new ExecuteAppFunctionRequest(
                             targetPackageName, functionIdentifier, extras, parameters);
@@ -75,17 +75,17 @@
      *
      * <p>The document may have missing parameters. Developers are advised to implement defensive
      * handling measures.
-     *
+     * <p>
      * TODO(b/357551503): Document how function parameters can be obtained for function execution
      */
     @NonNull
-    private final GenericDocument mParameters;
+    private final GenericDocumentWrapper mParameters;
 
     private ExecuteAppFunctionRequest(
             @NonNull String targetPackageName,
             @NonNull String functionIdentifier,
             @NonNull Bundle extras,
-            @NonNull GenericDocument parameters) {
+            @NonNull GenericDocumentWrapper parameters) {
         mTargetPackageName = Objects.requireNonNull(targetPackageName);
         mFunctionIdentifier = Objects.requireNonNull(functionIdentifier);
         mExtras = Objects.requireNonNull(extras);
@@ -117,7 +117,7 @@
      */
     @NonNull
     public GenericDocument getParameters() {
-        return mParameters;
+        return mParameters.getValue();
     }
 
     /**
@@ -152,7 +152,8 @@
         @NonNull
         private Bundle mExtras = Bundle.EMPTY;
         @NonNull
-        private GenericDocument mParameters = new GenericDocument.Builder<>("", "", "").build();
+        private GenericDocument mParameters =
+                new GenericDocument.Builder<>("", "", "").build();
 
         public Builder(@NonNull String targetPackageName, @NonNull String functionIdentifier) {
             mTargetPackageName = Objects.requireNonNull(targetPackageName);
@@ -173,7 +174,8 @@
          */
         @NonNull
         public Builder setParameters(@NonNull GenericDocument parameters) {
-            mParameters = Objects.requireNonNull(parameters);
+            Objects.requireNonNull(parameters);
+            mParameters = parameters;
             return this;
         }
 
@@ -183,7 +185,8 @@
         @NonNull
         public ExecuteAppFunctionRequest build() {
             return new ExecuteAppFunctionRequest(
-                    mTargetPackageName, mFunctionIdentifier, mExtras, mParameters);
+                    mTargetPackageName, mFunctionIdentifier, mExtras,
+                    new GenericDocumentWrapper(mParameters));
         }
     }
 }
diff --git a/core/java/android/app/appfunctions/ExecuteAppFunctionResponse.java b/core/java/android/app/appfunctions/ExecuteAppFunctionResponse.java
index 872327d..9fb3375 100644
--- a/core/java/android/app/appfunctions/ExecuteAppFunctionResponse.java
+++ b/core/java/android/app/appfunctions/ExecuteAppFunctionResponse.java
@@ -41,13 +41,16 @@
             new Creator<ExecuteAppFunctionResponse>() {
                 @Override
                 public ExecuteAppFunctionResponse createFromParcel(Parcel parcel) {
-                    GenericDocument result =
-                            Objects.requireNonNull(GenericDocument.createFromParcel(parcel));
+                    GenericDocumentWrapper resultWrapper =
+                            Objects.requireNonNull(
+                                    GenericDocumentWrapper
+                                            .CREATOR.createFromParcel(parcel));
                     Bundle extras = Objects.requireNonNull(
                             parcel.readBundle(Bundle.class.getClassLoader()));
                     int resultCode = parcel.readInt();
                     String errorMessage = parcel.readString8();
-                    return new ExecuteAppFunctionResponse(result, extras, resultCode, errorMessage);
+                    return new ExecuteAppFunctionResponse(
+                            resultWrapper, extras, resultCode, errorMessage);
                 }
 
                 @Override
@@ -127,7 +130,7 @@
      * <p>See {@link #getResultDocument} for more information on extracting the return value.
      */
     @NonNull
-    private final GenericDocument mResultDocument;
+    private final GenericDocumentWrapper mResultDocumentWrapper;
 
     /**
      * Returns the additional metadata data relevant to this function execution response.
@@ -135,17 +138,30 @@
     @NonNull
     private final Bundle mExtras;
 
-    private ExecuteAppFunctionResponse(@NonNull GenericDocument resultDocument,
+    private ExecuteAppFunctionResponse(@NonNull GenericDocumentWrapper resultDocumentWrapper,
                                        @NonNull Bundle extras,
                                        @ResultCode int resultCode,
                                        @Nullable String errorMessage) {
-        mResultDocument = Objects.requireNonNull(resultDocument);
+        mResultDocumentWrapper = Objects.requireNonNull(resultDocumentWrapper);
         mExtras = Objects.requireNonNull(extras);
         mResultCode = resultCode;
         mErrorMessage = errorMessage;
     }
 
     /**
+     * Returns result codes from throwable.
+     *
+     * @hide
+     */
+    @FlaggedApi(FLAG_ENABLE_APP_FUNCTION_MANAGER)
+    static @ResultCode int getResultCode(@NonNull Throwable t) {
+        if (t instanceof IllegalArgumentException) {
+            return ExecuteAppFunctionResponse.RESULT_INVALID_ARGUMENT;
+        }
+        return ExecuteAppFunctionResponse.RESULT_APP_UNKNOWN_ERROR;
+    }
+
+    /**
      * Returns a generic document containing the return value of the executed function.
      *
      * <p>The {@link #PROPERTY_RETURN_VALUE} key can be used to obtain the return value.</p>
@@ -166,7 +182,7 @@
      */
     @NonNull
     public GenericDocument getResultDocument() {
-        return mResultDocument;
+        return mResultDocumentWrapper.getValue();
     }
 
     /**
@@ -210,7 +226,7 @@
 
     @Override
     public void writeToParcel(@NonNull Parcel dest, int flags) {
-        mResultDocument.writeToParcel(dest, flags);
+        mResultDocumentWrapper.writeToParcel(dest, flags);
         dest.writeBundle(mExtras);
         dest.writeInt(mResultCode);
         dest.writeString8(mErrorMessage);
@@ -236,24 +252,13 @@
     }
 
     /**
-     * Returns result codes from throwable.
-     *
-     * @hide
-     */
-    static @ResultCode int getResultCode(@NonNull Throwable t) {
-        if (t instanceof IllegalArgumentException) {
-            return ExecuteAppFunctionResponse.RESULT_INVALID_ARGUMENT;
-        }
-        return ExecuteAppFunctionResponse.RESULT_APP_UNKNOWN_ERROR;
-    }
-
-    /**
      * The builder for creating {@link ExecuteAppFunctionResponse} instances.
      */
     @FlaggedApi(FLAG_ENABLE_APP_FUNCTION_MANAGER)
     public static final class Builder {
         @NonNull
-        private GenericDocument mResultDocument = new GenericDocument.Builder<>("", "", "").build();
+        private GenericDocument mResultDocument =
+                new GenericDocument.Builder<>("", "", "").build();
         @NonNull
         private Bundle mExtras = Bundle.EMPTY;
         private int mResultCode;
@@ -271,7 +276,8 @@
          * with a result code of {@link #RESULT_OK} and a resultDocument.
          */
         public Builder(@NonNull GenericDocument resultDocument) {
-            mResultDocument = Objects.requireNonNull(resultDocument);
+            Objects.requireNonNull(resultDocument);
+            mResultDocument = resultDocument;
             mResultCode = RESULT_OK;
         }
 
@@ -300,7 +306,8 @@
         @NonNull
         public ExecuteAppFunctionResponse build() {
             return new ExecuteAppFunctionResponse(
-                    mResultDocument, mExtras, mResultCode, mErrorMessage);
+                    new GenericDocumentWrapper(mResultDocument),
+                    mExtras, mResultCode, mErrorMessage);
         }
     }
 }
diff --git a/core/java/android/app/appfunctions/GenericDocumentWrapper.java b/core/java/android/app/appfunctions/GenericDocumentWrapper.java
new file mode 100644
index 0000000..8c76c8e
--- /dev/null
+++ b/core/java/android/app/appfunctions/GenericDocumentWrapper.java
@@ -0,0 +1,91 @@
+/*
+ * 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.app.appfunctions;
+
+import android.app.appsearch.GenericDocument;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+
+import java.util.Objects;
+
+/**
+ * The Parcelable object contains a {@link GenericDocument} to allow the parcelization of it
+ * exceeding the binder limit.
+ *
+ * <p>{#link {@link Parcel#writeBlob(byte[])}} could take care of whether to pass data via binder
+ * directly or Android shared memory if the data is large.
+ *
+ * @hide
+ * @see Parcel#writeBlob(byte[])
+ */
+public final class GenericDocumentWrapper implements Parcelable {
+    public static final Creator<GenericDocumentWrapper> CREATOR =
+            new Creator<>() {
+                @Override
+                public GenericDocumentWrapper createFromParcel(Parcel in) {
+                    byte[] dataBlob = Objects.requireNonNull(in.readBlob());
+                    Parcel unmarshallParcel = Parcel.obtain();
+                    try {
+                        unmarshallParcel.unmarshall(dataBlob, 0, dataBlob.length);
+                        unmarshallParcel.setDataPosition(0);
+                        return new GenericDocumentWrapper(
+                                GenericDocument.createFromParcel(unmarshallParcel));
+                    } finally {
+                        unmarshallParcel.recycle();
+                    }
+                }
+
+                @Override
+                public GenericDocumentWrapper[] newArray(int size) {
+                    return new GenericDocumentWrapper[size];
+                }
+            };
+    @NonNull
+    private final GenericDocument mGenericDocument;
+
+    public GenericDocumentWrapper(@NonNull GenericDocument genericDocument) {
+        mGenericDocument = Objects.requireNonNull(genericDocument);
+    }
+
+    /**
+     * Returns the wrapped {@link android.app.appsearch.GenericDocument}
+     */
+    @NonNull
+    public GenericDocument getValue() {
+        return mGenericDocument;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        Parcel parcel = Parcel.obtain();
+        try {
+            mGenericDocument.writeToParcel(parcel, flags);
+            byte[] bytes = parcel.marshall();
+            dest.writeBlob(bytes);
+        } finally {
+            parcel.recycle();
+        }
+
+    }
+}
diff --git a/core/java/android/security/OWNERS b/core/java/android/security/OWNERS
index 8bd6c85..c38ee08 100644
--- a/core/java/android/security/OWNERS
+++ b/core/java/android/security/OWNERS
@@ -3,6 +3,7 @@
 brambonne@google.com
 eranm@google.com
 jeffv@google.com
+tweek@google.com
 
 per-file *NetworkSecurityPolicy.java = file:net/OWNERS
 per-file Confirmation*.java = file:/keystore/OWNERS
diff --git a/core/java/android/view/PointerIcon.java b/core/java/android/view/PointerIcon.java
index 1535145..815fd1c 100644
--- a/core/java/android/view/PointerIcon.java
+++ b/core/java/android/view/PointerIcon.java
@@ -556,12 +556,9 @@
                                 + "is a different type from the others. All frames should be the "
                                 + "same type.");
                     }
-                    if (drawableFrame.getIntrinsicWidth() != width ||
-                        drawableFrame.getIntrinsicHeight() != height) {
-                        throw new IllegalArgumentException("The bitmap size of " + i + "-th frame "
-                                + "is different. All frames should have the exact same size and "
-                                + "share the same hotspot.");
-                    }
+                    // TODO(b/361232935): Check when bitmap size of the ith frame is different
+                    // drawableFrame.getIntrinsicWidth() != width ||
+                    // drawableFrame.getIntrinsicHeight() != height
                     if (isVectorAnimation) {
                         drawableFrame = getBitmapDrawableFromVectorDrawable(resources,
                                 (VectorDrawable) drawableFrame, pointerScale);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellCoroutinesModule.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellCoroutinesModule.kt
new file mode 100644
index 0000000..a489c4f
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellCoroutinesModule.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.dagger
+
+import com.android.wm.shell.common.ShellExecutor
+import com.android.wm.shell.shared.annotations.ShellBackgroundThread
+import com.android.wm.shell.shared.annotations.ShellMainThread
+import dagger.Module
+import dagger.Provides
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.asCoroutineDispatcher
+
+/** Providers for various WmShell-specific coroutines-related constructs. */
+@Module
+class WMShellCoroutinesModule {
+  @Provides
+  @ShellMainThread
+  fun provideMainDispatcher(@ShellMainThread mainExecutor: ShellExecutor): CoroutineDispatcher =
+      mainExecutor.asCoroutineDispatcher()
+
+  @Provides
+  @ShellBackgroundThread
+  fun provideBackgroundDispatcher(
+      @ShellBackgroundThread backgroundExecutor: ShellExecutor
+  ): CoroutineDispatcher = backgroundExecutor.asCoroutineDispatcher()
+
+  @Provides
+  @WMSingleton
+  @ShellMainThread
+  fun provideApplicationScope(
+      @ShellMainThread applicationDispatcher: CoroutineDispatcher,
+  ): CoroutineScope = CoroutineScope(applicationDispatcher)
+
+  @Provides
+  @WMSingleton
+  @ShellBackgroundThread
+  fun provideBackgroundCoroutineScope(
+      @ShellBackgroundThread backgroundDispatcher: CoroutineDispatcher,
+  ): CoroutineScope = CoroutineScope(backgroundDispatcher)
+
+  @Provides
+  @WMSingleton
+  @ShellBackgroundThread
+  fun provideBackgroundCoroutineContext(
+      @ShellBackgroundThread backgroundDispatcher: CoroutineDispatcher
+  ): CoroutineContext = backgroundDispatcher + SupervisorJob()
+}
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SnapResizeAppWindowWithButton.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SnapResizeAppWindowWithButton.kt
index 06775e1..685a3ba 100644
--- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SnapResizeAppWindowWithButton.kt
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SnapResizeAppWindowWithButton.kt
@@ -23,6 +23,7 @@
 import androidx.test.uiautomator.UiDevice
 import com.android.launcher3.tapl.LauncherInstrumentation
 import com.android.server.wm.flicker.helpers.DesktopModeAppHelper
+import com.android.server.wm.flicker.helpers.NonResizeableAppHelper
 import com.android.server.wm.flicker.helpers.SimpleAppHelper
 import com.android.window.flags.Flags
 import org.junit.After
@@ -36,13 +37,17 @@
 @Postsubmit
 open class SnapResizeAppWindowWithButton
 @JvmOverloads
-constructor(private val toLeft: Boolean = true) {
+constructor(private val toLeft: Boolean = true, private val isResizable: Boolean = true) {
 
     private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
     private val tapl = LauncherInstrumentation()
     private val wmHelper = WindowManagerStateHelper(instrumentation)
     private val device = UiDevice.getInstance(instrumentation)
-    private val testApp = DesktopModeAppHelper(SimpleAppHelper(instrumentation))
+    private val testApp = if (isResizable) {
+        DesktopModeAppHelper(SimpleAppHelper(instrumentation))
+    } else {
+        DesktopModeAppHelper(NonResizeableAppHelper(instrumentation))
+    }
 
     @Before
     fun setup() {
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SnapResizeAppWindowWithDrag.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SnapResizeAppWindowWithDrag.kt
index 871602f..8a4aa63 100644
--- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SnapResizeAppWindowWithDrag.kt
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SnapResizeAppWindowWithDrag.kt
@@ -23,6 +23,7 @@
 import androidx.test.uiautomator.UiDevice
 import com.android.launcher3.tapl.LauncherInstrumentation
 import com.android.server.wm.flicker.helpers.DesktopModeAppHelper
+import com.android.server.wm.flicker.helpers.NonResizeableAppHelper
 import com.android.server.wm.flicker.helpers.SimpleAppHelper
 import com.android.window.flags.Flags
 import org.junit.After
@@ -36,13 +37,17 @@
 @Postsubmit
 open class SnapResizeAppWindowWithDrag
 @JvmOverloads
-constructor(private val toLeft: Boolean = true) {
+constructor(private val toLeft: Boolean = true, private val isResizable: Boolean = true) {
 
     private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
     private val tapl = LauncherInstrumentation()
     private val wmHelper = WindowManagerStateHelper(instrumentation)
     private val device = UiDevice.getInstance(instrumentation)
-    private val testApp = DesktopModeAppHelper(SimpleAppHelper(instrumentation))
+    private val testApp = if (isResizable) {
+        DesktopModeAppHelper(SimpleAppHelper(instrumentation))
+    } else {
+        DesktopModeAppHelper(NonResizeableAppHelper(instrumentation))
+    }
 
     @Before
     fun setup() {
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 2e98c1f..f98b29a 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -484,6 +484,7 @@
 
         <activity android:name=".touchpad.tutorial.ui.view.TouchpadTutorialActivity"
             android:exported="true"
+            android:showForAllUsers="true"
             android:screenOrientation="userLandscape"
             android:theme="@style/Theme.AppCompat.NoActionBar">
             <intent-filter>
@@ -494,6 +495,7 @@
 
         <activity android:name=".inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity"
             android:exported="true"
+            android:showForAllUsers="true"
             android:screenOrientation="userLandscape"
             android:theme="@style/Theme.AppCompat.NoActionBar">
             <intent-filter>
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt
index f399436..0555346 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt
@@ -103,10 +103,10 @@
                 when {
                     isSplitShade -> UnsquishingQS(squishiness)
                     fromScene == Scenes.Shade && toScene == Scenes.QuickSettings -> {
-                        Expanding(progress)
+                        Expanding { progress }
                     }
                     fromScene == Scenes.QuickSettings && toScene == Scenes.Shade -> {
-                        Collapsing(progress)
+                        Collapsing { progress }
                     }
                     fromScene == Scenes.Shade || toScene == Scenes.Shade -> {
                         UnsquishingQQS(squishiness)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractorTest.kt
index 94fa9b9..e0a53f8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractorTest.kt
@@ -31,9 +31,8 @@
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
 import com.android.systemui.qs.tiles.impl.modes.domain.model.ModesTileModel
-import com.android.systemui.shared.notifications.data.repository.NotificationSettingsRepository
 import com.android.systemui.statusbar.policy.data.repository.fakeZenModeRepository
-import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor
+import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -44,7 +43,6 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.kotlin.mock
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
@@ -55,12 +53,7 @@
     private val dispatcher = kosmos.testDispatcher
     private val zenModeRepository = kosmos.fakeZenModeRepository
 
-    private val underTest =
-        ModesTileDataInteractor(
-            context,
-            ZenModeInteractor(zenModeRepository, mock<NotificationSettingsRepository>()),
-            dispatcher
-        )
+    private val underTest = ModesTileDataInteractor(context, kosmos.zenModeInteractor, dispatcher)
 
     @Before
     fun setUp() {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt
index d472d98..22913f1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt
@@ -255,7 +255,7 @@
             runCurrent()
             clearInvocations(qsImpl!!)
 
-            underTest.setState(QSSceneAdapter.State.Expanding(progress))
+            underTest.setState(QSSceneAdapter.State.Expanding { progress })
             with(qsImpl!!) {
                 verify(this).setQsVisible(true)
                 verify(this, never())
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterTest.kt
index 63ce67c..41b5988 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterTest.kt
@@ -20,7 +20,12 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.qs.ui.adapter.ExpandingSubject.Companion.assertThatExpanding
 import com.android.systemui.qs.ui.adapter.QSSceneAdapter.State.Companion.Collapsing
+import com.google.common.truth.FailureMetadata
+import com.google.common.truth.Subject
+import com.google.common.truth.Subject.Factory
+import com.google.common.truth.Truth.assertAbout
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -32,33 +37,59 @@
 
     @Test
     fun expanding_squishiness1() {
-        assertThat(QSSceneAdapter.State.Expanding(0.3f).squishiness()).isEqualTo(1f)
+        assertThat(QSSceneAdapter.State.Expanding { 0.3f }.squishiness()).isEqualTo(1f)
     }
 
     @Test
     fun expandingSpecialValues() {
-        assertThat(QSSceneAdapter.State.QQS).isEqualTo(QSSceneAdapter.State.Expanding(0f))
-        assertThat(QSSceneAdapter.State.QS).isEqualTo(QSSceneAdapter.State.Expanding(1f))
+        assertThatExpanding(QSSceneAdapter.State.QQS)
+            .isEqualTo(QSSceneAdapter.State.Expanding { 0f })
+        assertThatExpanding(QSSceneAdapter.State.QS)
+            .isEqualTo(QSSceneAdapter.State.Expanding { 1f })
     }
 
     @Test
     fun collapsing() {
         val collapsingProgress = 0.3f
-        assertThat(Collapsing(collapsingProgress))
-            .isEqualTo(QSSceneAdapter.State.Expanding(1 - collapsingProgress))
+        assertThatExpanding(Collapsing { collapsingProgress })
+            .isEqualTo(QSSceneAdapter.State.Expanding { 1 - collapsingProgress })
     }
 
     @Test
     fun unsquishingQQS_expansionSameAsQQS() {
         val squishiness = 0.6f
-        assertThat(QSSceneAdapter.State.UnsquishingQQS { squishiness }.expansion)
-            .isEqualTo(QSSceneAdapter.State.QQS.expansion)
+        assertThat(QSSceneAdapter.State.UnsquishingQQS { squishiness }.expansion())
+            .isEqualTo(QSSceneAdapter.State.QQS.expansion())
     }
 
     @Test
     fun unsquishingQS_expansionSameAsQS() {
         val squishiness = 0.6f
-        assertThat(QSSceneAdapter.State.UnsquishingQS { squishiness }.expansion)
-            .isEqualTo(QSSceneAdapter.State.QS.expansion)
+        assertThat(QSSceneAdapter.State.UnsquishingQS { squishiness }.expansion())
+            .isEqualTo(QSSceneAdapter.State.QS.expansion())
+    }
+}
+
+private class ExpandingSubject(
+    metadata: FailureMetadata,
+    private val actual: QSSceneAdapter.State.Expanding?
+) : Subject(metadata, actual) {
+    fun isEqualTo(expected: QSSceneAdapter.State.Expanding) {
+        isNotNull()
+        check("expansion()")
+            .that(actual?.expansion?.invoke())
+            .isEqualTo(expected.expansion.invoke())
+    }
+
+    companion object {
+        fun expanding(): Factory<ExpandingSubject, QSSceneAdapter.State.Expanding> {
+            return Factory { metadata: FailureMetadata, actual: QSSceneAdapter.State.Expanding? ->
+                ExpandingSubject(metadata, actual)
+            }
+        }
+
+        fun assertThatExpanding(actual: QSSceneAdapter.State.Expanding): ExpandingSubject {
+            return assertAbout(expanding()).that(actual)
+        }
     }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt
index 11504aa..20d3a7b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.policy.domain.interactor
 
+import android.app.AutomaticZenRule
 import android.app.NotificationManager.Policy
 import android.provider.Settings
 import android.provider.Settings.Secure.ZEN_DURATION
@@ -217,4 +218,35 @@
             assertThat(zenModeRepository.getModeActiveDuration(manualDnd.id))
                 .isEqualTo(Duration.ofMinutes(60))
         }
+
+    @Test
+    fun mainActiveMode_returnsMainActiveMode() =
+        testScope.runTest {
+            val mainActiveMode by collectLastValue(underTest.mainActiveMode)
+
+            zenModeRepository.addMode(id = "Bedtime", type = AutomaticZenRule.TYPE_BEDTIME)
+            zenModeRepository.addMode(id = "Other", type = AutomaticZenRule.TYPE_OTHER)
+
+            runCurrent()
+            assertThat(mainActiveMode).isNull()
+
+            zenModeRepository.activateMode("Other")
+            runCurrent()
+            assertThat(mainActiveMode).isNotNull()
+            assertThat(mainActiveMode!!.id).isEqualTo("Other")
+
+            zenModeRepository.activateMode("Bedtime")
+            runCurrent()
+            assertThat(mainActiveMode).isNotNull()
+            assertThat(mainActiveMode!!.id).isEqualTo("Bedtime")
+
+            zenModeRepository.deactivateMode("Other")
+            runCurrent()
+            assertThat(mainActiveMode).isNotNull()
+            assertThat(mainActiveMode!!.id).isEqualTo("Bedtime")
+
+            zenModeRepository.deactivateMode("Bedtime")
+            runCurrent()
+            assertThat(mainActiveMode).isNull()
+        }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
index 5d81d4f..9f9c8e9 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
@@ -53,6 +53,10 @@
 import com.android.compose.theme.PlatformTheme
 import com.android.systemui.compose.modifiers.sysuiResTag
 import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager
+import com.android.systemui.media.controls.ui.view.MediaHost
+import com.android.systemui.media.dagger.MediaModule.QS_PANEL
+import com.android.systemui.media.dagger.MediaModule.QUICK_QS_PANEL
 import com.android.systemui.plugins.qs.QS
 import com.android.systemui.plugins.qs.QSContainerController
 import com.android.systemui.qs.composefragment.viewmodel.QSFragmentComposeViewModel
@@ -65,6 +69,7 @@
 import com.android.systemui.util.LifecycleFragment
 import java.util.function.Consumer
 import javax.inject.Inject
+import javax.inject.Named
 import kotlinx.coroutines.awaitCancellation
 import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.flow.Flow
@@ -77,6 +82,8 @@
 @Inject
 constructor(
     private val qsFragmentComposeViewModelFactory: QSFragmentComposeViewModel.Factory,
+    @Named(QUICK_QS_PANEL) private val qqsMediaHost: MediaHost,
+    @Named(QS_PANEL) private val qsMediaHost: MediaHost,
 ) : LifecycleFragment(), QS {
 
     private val scrollListener = MutableStateFlow<QS.ScrollListener?>(null)
@@ -99,6 +106,8 @@
         QSComposeFragment.isUnexpectedlyInLegacyMode()
         viewModel = qsFragmentComposeViewModelFactory.create(lifecycleScope)
 
+        qqsMediaHost.init(MediaHierarchyManager.LOCATION_QQS)
+        qsMediaHost.init(MediaHierarchyManager.LOCATION_QS)
         setListenerCollections()
     }
 
@@ -274,7 +283,7 @@
     ) {}
 
     override fun isFullyCollapsed(): Boolean {
-        return !viewModel.isQSVisible
+        return viewModel.qsExpansionValue <= 0f
     }
 
     override fun setCollapsedMediaVisibilityChangedListener(listener: Consumer<Boolean>?) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt
index 9e109e4..7d52216 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt
@@ -18,6 +18,7 @@
 
 import android.content.res.Resources
 import android.graphics.Rect
+import androidx.annotation.FloatRange
 import androidx.lifecycle.LifecycleCoroutineScope
 import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
 import com.android.systemui.dagger.qualifiers.Main
@@ -79,11 +80,17 @@
             _qsVisible.value = value
         }
 
-    private val _qsExpansion = MutableStateFlow(0f)
+    // This can only be negative if undefined (in which case it will be -1f), else it will be
+    // in [0, 1]. In some cases, it could be set back to -1f internally to indicate that it's
+    // different to every value in [0, 1].
+    @FloatRange(from = -1.0, to = 1.0) private val _qsExpansion = MutableStateFlow(-1f)
     var qsExpansionValue: Float
         get() = _qsExpansion.value
         set(value) {
-            _qsExpansion.value = value
+            if (value < 0f) {
+                _qsExpansion.value = -1f
+            }
+            _qsExpansion.value = value.coerceIn(0f, 1f)
         }
 
     private val _panelFraction = MutableStateFlow(0f)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt
index dfcf216..ac6ebe7 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt
@@ -147,17 +147,17 @@
     sealed interface State {
 
         val isVisible: Boolean
-        val expansion: Float
+        val expansion: () -> Float
         val squishiness: () -> Float
 
         data object CLOSED : State {
             override val isVisible = false
-            override val expansion = 0f
+            override val expansion = { 0f }
             override val squishiness = { 1f }
         }
 
         /** State for expanding between QQS and QS */
-        data class Expanding(override val expansion: Float) : State {
+        class Expanding(override val expansion: () -> Float) : State {
             override val isVisible = true
             override val squishiness = { 1f }
         }
@@ -170,7 +170,7 @@
          */
         class UnsquishingQQS(override val squishiness: () -> Float) : State {
             override val isVisible = true
-            override val expansion = 0f
+            override val expansion = { 0f }
         }
 
         /**
@@ -181,16 +181,16 @@
          */
         class UnsquishingQS(override val squishiness: () -> Float) : State {
             override val isVisible = true
-            override val expansion = 1f
+            override val expansion = { 1f }
         }
 
         companion object {
             // These are special cases of the expansion.
-            val QQS = Expanding(0f)
-            val QS = Expanding(1f)
+            val QQS = Expanding { 0f }
+            val QS = Expanding { 1f }
 
             /** Collapsing from QS to QQS. [progress] is 0f in QS and 1f in QQS. */
-            fun Collapsing(progress: Float) = Expanding(1f - progress)
+            fun Collapsing(progress: () -> Float) = Expanding { 1f - progress() }
         }
     }
 }
@@ -418,14 +418,14 @@
 
     private fun QSImpl.applyState(state: QSSceneAdapter.State) {
         setQsVisible(state.isVisible)
-        setExpanded(state.isVisible && state.expansion > 0f)
+        setExpanded(state.isVisible && state.expansion() > 0f)
         setListening(state.isVisible)
     }
 
     override fun applyLatestExpansionAndSquishiness() {
         val qsImpl = _qsImpl.value
         val state = state.value
-        qsImpl?.setQsExpansion(state.expansion, 1f, 0f, state.squishiness())
+        qsImpl?.setQsExpansion(state.expansion(), 1f, 0f, state.squishiness())
     }
 
     override fun dump(pw: PrintWriter, args: Array<out String>) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt
index 7586133..f16fcb5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt
@@ -42,6 +42,7 @@
 class ZenModeInteractor
 @Inject
 constructor(
+    private val context: Context,
     private val zenModeRepository: ZenModeRepository,
     private val notificationSettingsRepository: NotificationSettingsRepository,
 ) {
@@ -78,6 +79,26 @@
     val activeModes: Flow<List<ZenMode>> =
         modes.map { modes -> modes.filter { mode -> mode.isActive } }.distinctUntilChanged()
 
+    /** Flow returning the most prioritized of the active modes, if any. */
+    val mainActiveMode: Flow<ZenMode?> =
+        activeModes.map { modes -> getMainActiveMode(modes) }.distinctUntilChanged()
+
+    /**
+     * Given the list of modes (which may include zero or more currently active modes), returns the
+     * most prioritized of the active modes, if any.
+     */
+    private fun getMainActiveMode(modes: List<ZenMode>): ZenMode? {
+        return modes.sortedWith(ZenMode.PRIORITIZING_COMPARATOR).firstOrNull { it.isActive }
+    }
+
+    suspend fun getModeIcon(mode: ZenMode): Icon {
+        return mode.getIcon(context, iconLoader).await().asIcon()
+    }
+
+    suspend fun getLockscreenModeIcon(mode: ZenMode): Icon {
+        return mode.getLockscreenIcon(context, iconLoader).await().asIcon()
+    }
+
     /**
      * Given the list of modes (which may include zero or more currently active modes), returns an
      * icon representing the active mode, if any (or, if multiple modes are active, to the most
@@ -86,16 +107,7 @@
      * package).
      */
     suspend fun getActiveModeIcon(context: Context, modes: List<ZenMode>): Icon? {
-        return modes
-            .sortedWith(ZenMode.PRIORITIZING_COMPARATOR)
-            .firstOrNull { it.isActive }
-            ?.getLockscreenIcon(context, iconLoader)
-            ?.await()
-            ?.asIcon()
-    }
-
-    suspend fun getModeIcon(context: Context, mode: ZenMode): Icon {
-        return mode.getIcon(context, iconLoader).await().asIcon()
+        return getMainActiveMode(modes)?.let { m -> getLockscreenModeIcon(m) }
     }
 
     fun activateMode(zenMode: ZenMode) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModel.kt
index 02b5e49..be90bec 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModel.kt
@@ -88,7 +88,7 @@
                 modesList.map { mode ->
                     ModeTileViewModel(
                         id = mode.id,
-                        icon = zenModeInteractor.getModeIcon(context, mode),
+                        icon = zenModeInteractor.getModeIcon(mode),
                         text = mode.name,
                         subtext = getTileSubtext(mode),
                         enabled = mode.isActive,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ModesTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ModesTileTest.kt
index c2f035f1..19735e2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ModesTileTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ModesTileTest.kt
@@ -93,7 +93,7 @@
     private val tileDataInteractor =
         ModesTileDataInteractor(
             context,
-            ZenModeInteractor(zenModeRepository, mock<NotificationSettingsRepository>()),
+            ZenModeInteractor(context, zenModeRepository, mock<NotificationSettingsRepository>()),
             testDispatcher
         )
     private val mapper =
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorKosmos.kt
index aef0828..66be7e7 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorKosmos.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.policy.domain.interactor
 
+import android.content.testableContext
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
 import com.android.systemui.shared.notifications.data.repository.notificationSettingsRepository
@@ -23,6 +24,7 @@
 
 val Kosmos.zenModeInteractor by Fixture {
     ZenModeInteractor(
+        context = testableContext,
         zenModeRepository = zenModeRepository,
         notificationSettingsRepository = notificationSettingsRepository,
     )