Merge "Create a QSSceneInteractor" into main
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/QuickSettings.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/QuickSettings.kt
deleted file mode 100644
index c84a5e9..0000000
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/QuickSettings.kt
+++ /dev/null
@@ -1,79 +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.qs.footer.ui.compose
-
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.defaultMinSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.unit.dp
-import com.android.compose.animation.scene.ElementKey
-import com.android.compose.animation.scene.SceneScope
-
-object QuickSettings {
-    object Elements {
-        // TODO RENAME
-        val Content = ElementKey("QuickSettingsContent")
-        val CollapsedGrid = ElementKey("QuickSettingsCollapsedGrid")
-        val FooterActions = ElementKey("QuickSettingsFooterActions")
-    }
-}
-
-@Composable
-fun SceneScope.QuickSettings(
-    modifier: Modifier = Modifier,
-) {
-    // TODO(b/272780058): implement.
-    Column(
-        modifier =
-            modifier
-                .element(QuickSettings.Elements.Content)
-                .fillMaxWidth()
-                .defaultMinSize(minHeight = 300.dp)
-                .clip(RoundedCornerShape(32.dp))
-                .background(MaterialTheme.colorScheme.primary)
-                .padding(16.dp),
-    ) {
-        Text(
-            text = "Quick settings grid",
-            modifier =
-                Modifier.element(QuickSettings.Elements.CollapsedGrid)
-                    .align(Alignment.CenterHorizontally),
-            style = MaterialTheme.typography.titleLarge,
-            color = MaterialTheme.colorScheme.onPrimary,
-        )
-        Spacer(modifier = Modifier.weight(1f))
-        Text(
-            text = "QS footer actions",
-            modifier =
-                Modifier.element(QuickSettings.Elements.FooterActions)
-                    .align(Alignment.CenterHorizontally),
-            style = MaterialTheme.typography.titleSmall,
-            color = MaterialTheme.colorScheme.onPrimary,
-        )
-    }
-}
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
new file mode 100644
index 0000000..28a4801
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt
@@ -0,0 +1,130 @@
+/*
+ * 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.qs.ui.composable
+
+import android.view.ContextThemeWrapper
+import android.view.View
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.defaultMinSize
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.viewinterop.AndroidView
+import com.android.compose.animation.scene.ElementKey
+import com.android.compose.animation.scene.SceneScope
+import com.android.compose.theme.colorAttr
+import com.android.systemui.qs.ui.adapter.QSSceneAdapter
+import com.android.systemui.res.R
+
+object QuickSettings {
+    object Elements {
+        // TODO RENAME
+        val Content = ElementKey("QuickSettingsContent")
+        val CollapsedGrid = ElementKey("QuickSettingsCollapsedGrid")
+        val FooterActions = ElementKey("QuickSettingsFooterActions")
+    }
+}
+
+@Composable
+private fun QuickSettingsTheme(content: @Composable () -> Unit) {
+    val context = LocalContext.current
+    val themedContext =
+        remember(context) { ContextThemeWrapper(context, R.style.Theme_SystemUI_QuickSettings) }
+    CompositionLocalProvider(LocalContext provides themedContext) { content() }
+}
+
+@Composable
+fun SceneScope.QuickSettings(
+    modifier: Modifier = Modifier,
+    qsSceneAdapter: QSSceneAdapter,
+    state: QSSceneAdapter.State
+) {
+    // TODO(b/272780058): implement.
+    Column(
+        modifier =
+            modifier
+                .element(QuickSettings.Elements.Content)
+                .fillMaxWidth()
+                .defaultMinSize(minHeight = 300.dp)
+                .clip(RoundedCornerShape(32.dp))
+                .background(MaterialTheme.colorScheme.primary)
+                .padding(1.dp),
+    ) {
+        QuickSettingsContent(qsSceneAdapter = qsSceneAdapter, state)
+    }
+}
+
+@Composable
+private fun QuickSettingsContent(
+    qsSceneAdapter: QSSceneAdapter,
+    state: QSSceneAdapter.State,
+    modifier: Modifier = Modifier,
+) {
+    val qsView by qsSceneAdapter.qsView.collectAsState(null)
+    QuickSettingsTheme {
+        val context = LocalContext.current
+
+        val frame by remember(context) { mutableStateOf(FrameLayout(context)) }
+
+        LaunchedEffect(key1 = context) {
+            if (qsView == null) {
+                qsSceneAdapter.inflate(context, frame)
+            }
+        }
+        qsView?.let {
+            it.attachToParent(frame)
+            AndroidView(
+                modifier = modifier.fillMaxSize().background(colorAttr(R.attr.underSurface)),
+                factory = { _ ->
+                    qsSceneAdapter.setState(state)
+                    frame
+                },
+                onRelease = { frame.removeAllViews() },
+                update = { qsSceneAdapter.setState(state) }
+            )
+        }
+    }
+}
+
+private fun View.attachToParent(parent: ViewGroup) {
+    if (this.parent != null && this.parent != parent) {
+        (this.parent as ViewGroup).removeView(this)
+    }
+    if (this.parent != parent) {
+        parent.addView(
+            this,
+            ViewGroup.LayoutParams.MATCH_PARENT,
+            ViewGroup.LayoutParams.MATCH_PARENT,
+        )
+    }
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
index a33eac5..b9451d1 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
@@ -17,43 +17,53 @@
 package com.android.systemui.qs.ui.composable
 
 import android.view.ViewGroup
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.expandVertically
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.shrinkVertically
 import androidx.compose.foundation.clickable
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxHeight
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
 import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.unit.dp
 import com.android.compose.animation.scene.SceneScope
 import com.android.compose.windowsizeclass.LocalWindowSizeClass
 import com.android.systemui.battery.BatteryMeterViewController
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.qs.footer.ui.compose.QuickSettings
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.qs.ui.adapter.QSSceneAdapter
 import com.android.systemui.qs.ui.viewmodel.QuickSettingsSceneViewModel
-import com.android.systemui.scene.shared.model.Direction
 import com.android.systemui.scene.shared.model.SceneKey
-import com.android.systemui.scene.shared.model.SceneModel
-import com.android.systemui.scene.shared.model.UserAction
 import com.android.systemui.scene.ui.composable.ComposableScene
 import com.android.systemui.shade.ui.composable.CollapsedShadeHeader
 import com.android.systemui.shade.ui.composable.ExpandedShadeHeader
+import com.android.systemui.shade.ui.composable.ShadeHeader
 import com.android.systemui.statusbar.phone.StatusBarIconController
 import com.android.systemui.statusbar.phone.StatusBarIconController.TintedIconManager
 import com.android.systemui.statusbar.phone.StatusBarLocation
 import javax.inject.Inject
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.stateIn
 
 /** The Quick Settings (AKA "QS") scene shows the quick setting tiles. */
 @SysUISingleton
 class QuickSettingsScene
 @Inject
 constructor(
+    @Application private val applicationScope: CoroutineScope,
     private val viewModel: QuickSettingsSceneViewModel,
     private val tintedIconManagerFactory: TintedIconManager.Factory,
     private val batteryMeterViewControllerFactory: BatteryMeterViewController.Factory,
@@ -61,14 +71,12 @@
 ) : ComposableScene {
     override val key = SceneKey.QuickSettings
 
-    private val _destinationScenes =
-        MutableStateFlow<Map<UserAction, SceneModel>>(
-                mapOf(
-                    UserAction.Swipe(Direction.UP) to SceneModel(SceneKey.Shade),
-                )
-            )
-            .asStateFlow()
-    override val destinationScenes: StateFlow<Map<UserAction, SceneModel>> = _destinationScenes
+    override val destinationScenes =
+        viewModel.destinationScenes.stateIn(
+            scope = applicationScope,
+            started = SharingStarted.WhileSubscribed(),
+            initialValue = emptyMap(),
+        )
 
     @Composable
     override fun SceneScope.Content(
@@ -93,6 +101,9 @@
     modifier: Modifier = Modifier,
 ) {
     // TODO(b/280887232): implement the real UI.
+    val isCustomizing by viewModel.qsSceneAdapter.isCustomizing.collectAsState()
+    val collapsedHeaderHeight =
+        with(LocalDensity.current) { ShadeHeader.Dimensions.CollapsedHeight.roundToPx() }
     Column(
         horizontalAlignment = Alignment.CenterHorizontally,
         modifier =
@@ -103,12 +114,27 @@
     ) {
         when (LocalWindowSizeClass.current.widthSizeClass) {
             WindowWidthSizeClass.Compact ->
-                ExpandedShadeHeader(
-                    viewModel = viewModel.shadeHeaderViewModel,
-                    createTintedIconManager = createTintedIconManager,
-                    createBatteryMeterViewController = createBatteryMeterViewController,
-                    statusBarIconController = statusBarIconController,
-                )
+                AnimatedVisibility(
+                    visible = !isCustomizing,
+                    enter =
+                        expandVertically(
+                            animationSpec = tween(1000),
+                            initialHeight = { collapsedHeaderHeight },
+                        ) + fadeIn(tween(1000)),
+                    exit =
+                        shrinkVertically(
+                            animationSpec = tween(1000),
+                            targetHeight = { collapsedHeaderHeight },
+                            shrinkTowards = Alignment.Top,
+                        ) + fadeOut(tween(1000)),
+                ) {
+                    ExpandedShadeHeader(
+                        viewModel = viewModel.shadeHeaderViewModel,
+                        createTintedIconManager = createTintedIconManager,
+                        createBatteryMeterViewController = createBatteryMeterViewController,
+                        statusBarIconController = statusBarIconController,
+                    )
+                }
             else ->
                 CollapsedShadeHeader(
                     viewModel = viewModel.shadeHeaderViewModel,
@@ -118,6 +144,10 @@
                 )
         }
         Spacer(modifier = Modifier.height(16.dp))
-        QuickSettings()
+        QuickSettings(
+            modifier = Modifier.fillMaxHeight(),
+            viewModel.qsSceneAdapter,
+            QSSceneAdapter.State.QS
+        )
     }
 }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToShadeTransition.kt
index 7ecfb62..fadbdce 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToShadeTransition.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToShadeTransition.kt
@@ -4,7 +4,7 @@
 import com.android.compose.animation.scene.Edge
 import com.android.compose.animation.scene.TransitionBuilder
 import com.android.systemui.notifications.ui.composable.Notifications
-import com.android.systemui.qs.footer.ui.compose.QuickSettings
+import com.android.systemui.qs.ui.composable.QuickSettings
 import com.android.systemui.shade.ui.composable.Shade
 
 fun TransitionBuilder.lockscreenToShadeTransition() {
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromShadeToQuickSettingsTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromShadeToQuickSettingsTransition.kt
index be85bee..5616175 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromShadeToQuickSettingsTransition.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromShadeToQuickSettingsTransition.kt
@@ -4,7 +4,7 @@
 import com.android.compose.animation.scene.Edge
 import com.android.compose.animation.scene.TransitionBuilder
 import com.android.systemui.notifications.ui.composable.Notifications
-import com.android.systemui.qs.footer.ui.compose.QuickSettings
+import com.android.systemui.qs.ui.composable.QuickSettings
 import com.android.systemui.shade.ui.composable.ShadeHeader
 
 fun TransitionBuilder.shadeToQuickSettingsTransition() {
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
index 13ebdf9..a02f046 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
@@ -25,6 +25,7 @@
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.wrapContentHeight
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.runtime.Composable
@@ -37,7 +38,8 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.notifications.ui.composable.Notifications
-import com.android.systemui.qs.footer.ui.compose.QuickSettings
+import com.android.systemui.qs.ui.adapter.QSSceneAdapter
+import com.android.systemui.qs.ui.composable.QuickSettings
 import com.android.systemui.scene.shared.model.Direction
 import com.android.systemui.scene.shared.model.SceneKey
 import com.android.systemui.scene.shared.model.SceneModel
@@ -152,7 +154,11 @@
                 statusBarIconController = statusBarIconController,
             )
             Spacer(modifier = Modifier.height(16.dp))
-            QuickSettings(modifier = Modifier.height(160.dp))
+            QuickSettings(
+                modifier = Modifier.wrapContentHeight(),
+                viewModel.qsSceneAdapter,
+                QSSceneAdapter.State.QQS
+            )
             Spacer(modifier = Modifier.height(16.dp))
             Notifications(modifier = Modifier.weight(1f))
         }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
index b0f54ee..0644237 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
@@ -28,8 +28,8 @@
 import android.widget.FrameLayout;
 
 import com.android.systemui.Dumpable;
-import com.android.systemui.res.R;
 import com.android.systemui.qs.customize.QSCustomizer;
+import com.android.systemui.res.R;
 import com.android.systemui.shade.TouchLogger;
 import com.android.systemui.util.LargeScreenUtils;
 
@@ -59,6 +59,8 @@
     private boolean mClippingEnabled;
     private boolean mIsFullWidth;
 
+    private boolean mSceneContainerEnabled;
+
     public QSContainerImpl(Context context, AttributeSet attrs) {
         super(context, attrs);
     }
@@ -72,6 +74,10 @@
         setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
     }
 
+    void setSceneContainerEnabled(boolean enabled) {
+        mSceneContainerEnabled = enabled;
+    }
+
     @Override
     public boolean hasOverlappingRendering() {
         return false;
@@ -161,7 +167,7 @@
         }
         mQSPanelContainer.setPaddingRelative(
                 mQSPanelContainer.getPaddingStart(),
-                topPadding,
+                mSceneContainerEnabled ? 0 : topPadding,
                 mQSPanelContainer.getPaddingEnd(),
                 mQSPanelContainer.getPaddingBottom());
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImplController.java b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImplController.java
index 73a5faa..7b001c7 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImplController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImplController.java
@@ -24,6 +24,7 @@
 
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.qs.dagger.QSScope;
+import com.android.systemui.scene.shared.flag.SceneContainerFlags;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.util.ViewController;
 
@@ -44,6 +45,7 @@
             mView.updateResources(mQsPanelController, mQuickStatusBarHeaderController);
         }
     };
+    private final boolean mSceneContainerEnabled;
 
     private final View.OnTouchListener mContainerTouchHandler = new View.OnTouchListener() {
         @Override
@@ -64,18 +66,21 @@
             QSPanelController qsPanelController,
             QuickStatusBarHeaderController quickStatusBarHeaderController,
             ConfigurationController configurationController,
-            FalsingManager falsingManager) {
+            FalsingManager falsingManager,
+            SceneContainerFlags sceneContainerFlags) {
         super(view);
         mQsPanelController = qsPanelController;
         mQuickStatusBarHeaderController = quickStatusBarHeaderController;
         mConfigurationController = configurationController;
         mFalsingManager = falsingManager;
         mQSPanelContainer = mView.getQSPanelContainer();
+        mSceneContainerEnabled = sceneContainerFlags.isEnabled();
     }
 
     @Override
     public void onInit() {
         mQuickStatusBarHeaderController.init();
+        mView.setSceneContainerEnabled(mSceneContainerEnabled);
     }
 
     public void setListening(boolean listening) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSImpl.java b/packages/SystemUI/src/com/android/systemui/qs/QSImpl.java
index fab7e95..7f91fd2 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSImpl.java
@@ -97,6 +97,7 @@
     private boolean mStackScrollerOverscrolling;
 
     private QSAnimator mQSAnimator;
+    @Nullable
     private HeightListener mPanelView;
     private QSSquishinessController mQSSquishinessController;
     protected QuickStatusBarHeader mHeader;
@@ -340,6 +341,7 @@
         }
         if (mQSCustomizerController != null) {
             mQSCustomizerController.setQs(null);
+            mQSCustomizerController.setContainerController(null);
         }
         mScrollListener = null;
         if (mContainer != null) {
@@ -347,6 +349,10 @@
         }
         mDumpManager.unregisterDumpable(getClass().getSimpleName());
         mListeningAndVisibilityLifecycleOwner.destroy();
+        ViewGroup parent = ((ViewGroup) getView().getParent());
+        if (parent != null) {
+            parent.removeView(getView());
+        }
     }
 
     public void onSaveInstanceState(Bundle outState) {
@@ -853,6 +859,10 @@
         mQSCustomizerController.hide();
     }
 
+    public void closeCustomizerImmediately() {
+        mQSCustomizerController.hide(false);
+    }
+
     public void notifyCustomizeChanged() {
         // The customize state changed, so our height changed.
         mContainer.updateExpansion();
@@ -863,7 +873,9 @@
         mHeader.setVisibility(!customizing ? View.VISIBLE : View.INVISIBLE);
         // Let the panel know the position changed and it needs to update where notifications
         // and whatnot are.
-        mPanelView.onQsHeightChanged();
+        if (mPanelView != null) {
+            mPanelView.onQsHeightChanged();
+        }
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
index 3227f75..ddd7d67 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
@@ -39,9 +39,9 @@
 
 import com.android.internal.logging.UiEventLogger;
 import com.android.internal.widget.RemeasuringLinearLayout;
-import com.android.systemui.res.R;
 import com.android.systemui.plugins.qs.QSTile;
 import com.android.systemui.qs.logging.QSLogger;
+import com.android.systemui.res.R;
 import com.android.systemui.settings.brightness.BrightnessSliderController;
 import com.android.systemui.tuner.TunerService;
 import com.android.systemui.tuner.TunerService.Tunable;
@@ -110,6 +110,8 @@
      */
     private boolean mCanCollapse = true;
 
+    private boolean mSceneContainerEnabled;
+
     public QSPanel(Context context, AttributeSet attrs) {
         super(context, attrs);
         mUsingMediaPlayer = useQsMediaPlayer(context);
@@ -153,6 +155,13 @@
         }
     }
 
+    void setSceneContainerEnabled(boolean enabled) {
+        mSceneContainerEnabled = enabled;
+        if (mSceneContainerEnabled) {
+            updatePadding();
+        }
+    }
+
     protected void setHorizontalContentContainerClipping() {
         mHorizontalContentContainer.setClipChildren(true);
         mHorizontalContentContainer.setClipToPadding(false);
@@ -375,7 +384,7 @@
         int paddingTop = res.getDimensionPixelSize(R.dimen.qs_panel_padding_top);
         int paddingBottom = res.getDimensionPixelSize(R.dimen.qs_panel_padding_bottom);
         setPaddingRelative(getPaddingStart(),
-                paddingTop,
+                mSceneContainerEnabled ? 0 : paddingTop,
                 getPaddingEnd(),
                 paddingBottom);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java
index 6bbdc54..5eb9620 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java
@@ -34,6 +34,7 @@
 import com.android.systemui.qs.customize.QSCustomizerController;
 import com.android.systemui.qs.dagger.QSScope;
 import com.android.systemui.qs.logging.QSLogger;
+import com.android.systemui.scene.shared.flag.SceneContainerFlags;
 import com.android.systemui.settings.brightness.BrightnessController;
 import com.android.systemui.settings.brightness.BrightnessMirrorHandler;
 import com.android.systemui.settings.brightness.BrightnessSliderController;
@@ -61,6 +62,8 @@
     private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
     private boolean mListening;
 
+    private final boolean mSceneContainerEnabled;
+
     private View.OnTouchListener mTileLayoutTouchListener = new View.OnTouchListener() {
         @Override
         public boolean onTouch(View v, MotionEvent event) {
@@ -82,7 +85,8 @@
             BrightnessSliderController.Factory brightnessSliderFactory,
             FalsingManager falsingManager,
             StatusBarKeyguardViewManager statusBarKeyguardViewManager,
-            SplitShadeStateController splitShadeStateController) {
+            SplitShadeStateController splitShadeStateController,
+            SceneContainerFlags sceneContainerFlags) {
         super(view, qsHost, qsCustomizerController, usingMediaPlayer, mediaHost,
                 metricsLogger, uiEventLogger, qsLogger, dumpManager, splitShadeStateController);
         mTunerService = tunerService;
@@ -96,6 +100,7 @@
         mBrightnessController = brightnessControllerFactory.create(mBrightnessSliderController);
         mBrightnessMirrorHandler = new BrightnessMirrorHandler(mBrightnessController);
         mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
+        mSceneContainerEnabled = sceneContainerFlags.isEnabled();
     }
 
     @Override
@@ -116,6 +121,7 @@
 
         mTunerService.addTunable(mView, QS_SHOW_BRIGHTNESS);
         mView.updateResources();
+        mView.setSceneContainerEnabled(mSceneContainerEnabled);
         if (mView.isListening()) {
             refreshAllTiles();
         }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java
index 67c4208..c657b55 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java
@@ -40,6 +40,8 @@
 
     protected QuickQSPanel mHeaderQsPanel;
 
+    private boolean mSceneContainerEnabled;
+
     public QuickStatusBarHeader(Context context, AttributeSet attrs) {
         super(context, attrs);
     }
@@ -52,6 +54,13 @@
         updateResources();
     }
 
+    void setSceneContainerEnabled(boolean enabled) {
+        mSceneContainerEnabled = enabled;
+        if (mSceneContainerEnabled) {
+            updateResources();
+        }
+    }
+
     @Override
     protected void onConfigurationChanged(Configuration newConfig) {
         super.onConfigurationChanged(newConfig);
@@ -82,7 +91,9 @@
         setLayoutParams(lp);
 
         MarginLayoutParams qqsLP = (MarginLayoutParams) mHeaderQsPanel.getLayoutParams();
-        if (largeScreenHeaderActive) {
+        if (mSceneContainerEnabled) {
+            qqsLP.topMargin = 0;
+        } else if (largeScreenHeaderActive) {
             qqsLP.topMargin = mContext.getResources()
                     .getDimensionPixelSize(R.dimen.qqs_layout_margin_top);
         } else {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeaderController.java b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeaderController.java
index 64960e6..1d92d78 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeaderController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeaderController.java
@@ -17,6 +17,7 @@
 package com.android.systemui.qs;
 
 import com.android.systemui.qs.dagger.QSScope;
+import com.android.systemui.scene.shared.flag.SceneContainerFlags;
 import com.android.systemui.util.ViewController;
 
 import javax.inject.Inject;
@@ -29,17 +30,20 @@
 
     private final QuickQSPanelController mQuickQSPanelController;
     private boolean mListening;
+    private final boolean mSceneContainerEnabled;
 
     @Inject
     QuickStatusBarHeaderController(QuickStatusBarHeader view,
-            QuickQSPanelController quickQSPanelController
+            QuickQSPanelController quickQSPanelController,
+            SceneContainerFlags sceneContainerFlags
     ) {
         super(view);
         mQuickQSPanelController = quickQSPanelController;
+        mSceneContainerEnabled = sceneContainerFlags.isEnabled();
     }
-
     @Override
     protected void onViewAttached() {
+        mView.setSceneContainerEnabled(mSceneContainerEnabled);
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizerController.java b/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizerController.java
index 024e760..c28371c 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizerController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizerController.java
@@ -244,7 +244,12 @@
 
     /** Hice the customizer. */
     public void hide() {
-        final boolean animate = mScreenLifecycle.getScreenState() != ScreenLifecycle.SCREEN_OFF;
+        hide(true);
+    }
+
+    public void hide(boolean animated) {
+        final boolean animate = animated
+                && mScreenLifecycle.getScreenState() != ScreenLifecycle.SCREEN_OFF;
         if (mView.isShown()) {
             mUiEventLogger.log(QSEditEvent.QS_EDIT_CLOSED);
             mToolbar.dismissPopupMenus();
diff --git a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java
index bd4c6e1..1aef920 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java
@@ -32,6 +32,8 @@
 import com.android.systemui.qs.pipeline.dagger.QSPipelineModule;
 import com.android.systemui.qs.tileimpl.QSTileImpl;
 import com.android.systemui.qs.tiles.di.QSTilesModule;
+import com.android.systemui.qs.ui.adapter.QSSceneAdapter;
+import com.android.systemui.qs.ui.adapter.QSSceneAdapterImpl;
 import com.android.systemui.statusbar.phone.AutoTileManager;
 import com.android.systemui.statusbar.phone.ManagedProfileController;
 import com.android.systemui.statusbar.policy.CastController;
@@ -42,18 +44,19 @@
 import com.android.systemui.statusbar.policy.WalletController;
 import com.android.systemui.util.settings.SecureSettings;
 
-import dagger.Module;
-import dagger.Provides;
-import dagger.multibindings.Multibinds;
-
 import java.util.Map;
 
 import javax.inject.Named;
 
+import dagger.Binds;
+import dagger.Module;
+import dagger.Provides;
+import dagger.multibindings.Multibinds;
+
 /**
  * Module for QS dependencies
  */
-@Module(subcomponents = {QSFragmentComponent.class, QSFlexiglassComponent.class},
+@Module(subcomponents = {QSFragmentComponent.class, QSSceneComponent.class},
         includes = {
                 MediaModule.class,
                 QSExternalModule.class,
@@ -110,4 +113,7 @@
         manager.init();
         return manager;
     }
+
+    @Binds
+    QSSceneAdapter bindsQsSceneInteractor(QSSceneAdapterImpl impl);
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSFlexiglassComponent.kt b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSSceneComponent.kt
similarity index 82%
rename from packages/SystemUI/src/com/android/systemui/qs/dagger/QSFlexiglassComponent.kt
rename to packages/SystemUI/src/com/android/systemui/qs/dagger/QSSceneComponent.kt
index ba1aa62..b942368 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSFlexiglassComponent.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSSceneComponent.kt
@@ -21,12 +21,12 @@
 import dagger.BindsInstance
 import dagger.Subcomponent
 
-@Subcomponent(modules = [QSFlexiglassModule::class])
+@Subcomponent(modules = [QSSceneModule::class])
 @QSScope
-interface QSFlexiglassComponent : QSComponent {
+interface QSSceneComponent : QSComponent {
 
     @Subcomponent.Factory
     interface Factory {
-        fun create(@RootView @BindsInstance rootView: View): QSFlexiglassComponent
+        fun create(@RootView @BindsInstance rootView: View): QSSceneComponent
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSFlexiglassModule.kt b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSSceneModule.kt
similarity index 97%
rename from packages/SystemUI/src/com/android/systemui/qs/dagger/QSFlexiglassModule.kt
rename to packages/SystemUI/src/com/android/systemui/qs/dagger/QSSceneModule.kt
index 36fac44..446cb62 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSFlexiglassModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSSceneModule.kt
@@ -24,7 +24,7 @@
 import javax.inject.Named
 
 @Module(includes = [QSScopeModule::class])
-interface QSFlexiglassModule {
+interface QSSceneModule {
 
     @Module
     companion object {
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
new file mode 100644
index 0000000..b4340f5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt
@@ -0,0 +1,163 @@
+/*
+ * 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.qs.ui.adapter
+
+import android.content.Context
+import android.os.Bundle
+import android.view.View
+import android.view.ViewGroup
+import androidx.annotation.VisibleForTesting
+import androidx.asynclayoutinflater.view.AsyncLayoutInflater
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.plugins.qs.QSContainerController
+import com.android.systemui.qs.QSImpl
+import com.android.systemui.qs.dagger.QSSceneComponent
+import com.android.systemui.res.R
+import com.android.systemui.util.kotlin.sample
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import javax.inject.Inject
+import javax.inject.Provider
+import kotlin.coroutines.resume
+import kotlin.coroutines.suspendCoroutine
+
+// TODO(307945185) Split View concerns into a ViewBinder
+/** Adapter to use between Scene system and [QSImpl] */
+interface QSSceneAdapter {
+    /** Whether [QSImpl] is currently customizing */
+    val isCustomizing: StateFlow<Boolean>
+
+    /**
+     * A view with the QS content ([QSContainerImpl]), managed by an instance of [QSImpl] tracked by
+     * the interactor.
+     */
+    val qsView: Flow<View>
+
+    /**
+     * Inflate an instance of [QSImpl] for this context. Once inflated, it will be available in
+     * [qsView]
+     */
+    suspend fun inflate(context: Context, parent: ViewGroup? = null)
+
+    /** Set the current state for QS. [state] must not be [State.INITIAL]. */
+    fun setState(state: State)
+
+    sealed class State(
+        val isVisible: Boolean,
+        val expansion: Float,
+    ) {
+        data object CLOSED : State(false, 0f)
+        data object QQS : State(true, 0f)
+        data object QS : State(true, 1f)
+    }
+}
+
+@SysUISingleton
+class QSSceneAdapterImpl
+@VisibleForTesting
+constructor(
+    private val qsSceneComponentFactory: QSSceneComponent.Factory,
+    private val qsImplProvider: Provider<QSImpl>,
+    @Main private val mainDispatcher: CoroutineDispatcher,
+    @Application applicationScope: CoroutineScope,
+    private val asyncLayoutInflaterFactory: (Context) -> AsyncLayoutInflater,
+) : QSContainerController, QSSceneAdapter {
+
+    @Inject
+    constructor(
+        qsSceneComponentFactory: QSSceneComponent.Factory,
+        qsImplProvider: Provider<QSImpl>,
+        @Main dispatcher: CoroutineDispatcher,
+        @Application scope: CoroutineScope,
+    ) : this(qsSceneComponentFactory, qsImplProvider, dispatcher, scope, ::AsyncLayoutInflater)
+
+    private val state = MutableStateFlow<QSSceneAdapter.State>(QSSceneAdapter.State.CLOSED)
+    private val _isCustomizing: MutableStateFlow<Boolean> = MutableStateFlow(false)
+    override val isCustomizing = _isCustomizing.asStateFlow()
+
+    private val _qsImpl: MutableStateFlow<QSImpl?> = MutableStateFlow(null)
+    val qsImpl = _qsImpl.asStateFlow()
+    override val qsView: Flow<View> = _qsImpl.map { it?.view }.filterNotNull()
+
+    init {
+        applicationScope.launch {
+            state.sample(_isCustomizing, ::Pair).collect { (state, customizing) ->
+                _qsImpl.value?.apply {
+                    if (state != QSSceneAdapter.State.QS && customizing) {
+                        this@apply.closeCustomizerImmediately()
+                    }
+                    applyState(state)
+                }
+            }
+        }
+    }
+
+    override fun setCustomizerAnimating(animating: Boolean) {}
+
+    override fun setCustomizerShowing(showing: Boolean) {
+        _isCustomizing.value = showing
+    }
+
+    override fun setCustomizerShowing(showing: Boolean, animationDuration: Long) {
+        setCustomizerShowing(showing)
+    }
+
+    override fun setDetailShowing(showing: Boolean) {}
+
+    override suspend fun inflate(context: Context, parent: ViewGroup?) {
+        withContext(mainDispatcher) {
+            val inflater = asyncLayoutInflaterFactory(context)
+            val view = suspendCoroutine { continuation ->
+                inflater.inflate(R.layout.qs_panel, parent) { view, _, _ ->
+                    continuation.resume(view)
+                }
+            }
+            val bundle = Bundle()
+            _qsImpl.value?.onSaveInstanceState(bundle)
+            _qsImpl.value?.onDestroy()
+            val component = qsSceneComponentFactory.create(view)
+            val qs = qsImplProvider.get()
+            qs.onCreate(null)
+            qs.onComponentCreated(component, bundle)
+            _qsImpl.value = qs
+            qs.view.setPadding(0, 0, 0, 0)
+            qs.setContainerController(this@QSSceneAdapterImpl)
+            qs.applyState(state.value)
+        }
+    }
+    override fun setState(state: QSSceneAdapter.State) {
+        this.state.value = state
+    }
+
+    private fun QSImpl.applyState(state: QSSceneAdapter.State) {
+        setQsVisible(state.isVisible)
+        setExpanded(state.isVisible)
+        setListening(state.isVisible)
+        setQsExpansion(state.expansion, 1f, 0f, 1f)
+        setTransitionToFullShadeProgress(false, 1f, 1f)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt
index 5993cf1..3941fd7 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt
@@ -18,8 +18,14 @@
 
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
+import com.android.systemui.qs.ui.adapter.QSSceneAdapter
+import com.android.systemui.scene.shared.model.Direction
+import com.android.systemui.scene.shared.model.SceneKey
+import com.android.systemui.scene.shared.model.SceneModel
+import com.android.systemui.scene.shared.model.UserAction
 import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel
 import javax.inject.Inject
+import kotlinx.coroutines.flow.map
 
 /** Models UI state and handles user input for the quick settings scene. */
 @SysUISingleton
@@ -28,7 +34,20 @@
 constructor(
     private val deviceEntryInteractor: DeviceEntryInteractor,
     val shadeHeaderViewModel: ShadeHeaderViewModel,
+    val qsSceneAdapter: QSSceneAdapter,
 ) {
     /** Notifies that some content in quick settings was clicked. */
     fun onContentClicked() = deviceEntryInteractor.attemptDeviceEntry()
+
+    val destinationScenes =
+        qsSceneAdapter.isCustomizing.map { customizing ->
+            if (customizing) {
+                mapOf<UserAction, SceneModel>(UserAction.Back to SceneModel(SceneKey.QuickSettings))
+            } else {
+                mapOf(
+                    UserAction.Back to SceneModel(SceneKey.Shade),
+                    UserAction.Swipe(Direction.UP) to SceneModel(SceneKey.Shade),
+                )
+            }
+        }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java
index 5a4d876..4ca763f 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java
@@ -106,12 +106,12 @@
 
 import dalvik.annotation.optimization.NeverCompile;
 
-import dagger.Lazy;
-
 import java.io.PrintWriter;
 
 import javax.inject.Inject;
 
+import dagger.Lazy;
+
 /** Handles QuickSettings touch handling, expansion and animation state
  * TODO (b/264460656) make this dumpable
  */
@@ -720,7 +720,9 @@
 
     /** Closes the Qs customizer. */
     public void closeQsCustomizer() {
-        mQs.closeCustomizer();
+        if (mQs != null) {
+            mQs.closeCustomizer();
+        }
     }
 
     /** Returns whether touches from the notification panel should be disallowed */
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt
index 20b9ede..af88081 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt
@@ -19,13 +19,14 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
+import com.android.systemui.qs.ui.adapter.QSSceneAdapter
 import com.android.systemui.scene.shared.model.SceneKey
-import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.stateIn
+import javax.inject.Inject
 
 /** Models UI state and handles user input for the shade scene. */
 @SysUISingleton
@@ -34,6 +35,7 @@
 constructor(
     @Application private val applicationScope: CoroutineScope,
     private val deviceEntryInteractor: DeviceEntryInteractor,
+    val qsSceneAdapter: QSSceneAdapter,
     val shadeHeaderViewModel: ShadeHeaderViewModel,
 ) {
     /** The key of the scene we should switch to when swiping up. */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index 4586f58..cd7a9ea 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -160,6 +160,7 @@
 import com.android.systemui.qs.QSPanelController;
 import com.android.systemui.res.R;
 import com.android.systemui.scene.domain.interactor.WindowRootViewVisibilityInteractor;
+import com.android.systemui.scene.shared.flag.SceneContainerFlags;
 import com.android.systemui.scrim.ScrimView;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.settings.brightness.BrightnessSliderController;
@@ -584,6 +585,8 @@
 
     private final InteractionJankMonitor mJankMonitor;
 
+    private final SceneContainerFlags mSceneContainerFlags;
+
     /**
      * Public constructor for CentralSurfaces.
      *
@@ -697,7 +700,8 @@
             AlternateBouncerInteractor alternateBouncerInteractor,
             UserTracker userTracker,
             Provider<FingerprintManager> fingerprintManager,
-            ActivityStarter activityStarter
+            ActivityStarter activityStarter,
+            SceneContainerFlags sceneContainerFlags
     ) {
         mContext = context;
         mNotificationsController = notificationsController;
@@ -794,6 +798,7 @@
         mUserTracker = userTracker;
         mFingerprintManager = fingerprintManager;
         mActivityStarter = activityStarter;
+        mSceneContainerFlags = sceneContainerFlags;
 
         mLockscreenShadeTransitionController = lockscreenShadeTransitionController;
         mStartingSurfaceOptional = startingSurfaceOptional;
@@ -1248,7 +1253,7 @@
 
         // Set up the quick settings tile panel
         final View container = getNotificationShadeWindowView().findViewById(R.id.qs_frame);
-        if (container != null) {
+        if (container != null && !mSceneContainerFlags.isEnabled()) {
             FragmentHostManager fragmentHostManager =
                     mFragmentService.getFragmentHostManager(container);
             ExtensionFragmentListener.attachExtensonToFragment(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt
index 5e4c954..1f7a029 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt
@@ -16,6 +16,7 @@
 import com.android.systemui.plugins.qs.QSTile
 import com.android.systemui.qs.customize.QSCustomizerController
 import com.android.systemui.qs.logging.QSLogger
+import com.android.systemui.scene.shared.flag.FakeSceneContainerFlags
 import com.android.systemui.settings.brightness.BrightnessController
 import com.android.systemui.settings.brightness.BrightnessSliderController
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
@@ -61,6 +62,8 @@
     @Mock private lateinit var configuration: Configuration
     @Mock private lateinit var pagedTileLayout: PagedTileLayout
 
+    private val sceneContainerFlags = FakeSceneContainerFlags()
+
     private lateinit var controller: QSPanelController
     private val testableResources: TestableResources = mContext.orCreateTestableResources
 
@@ -96,7 +99,8 @@
             brightnessSliderFactory,
             falsingManager,
             statusBarKeyguardViewManager,
-                ResourcesSplitShadeStateController()
+            ResourcesSplitShadeStateController(),
+            sceneContainerFlags,
         )
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickStatusBarHeaderControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QuickStatusBarHeaderControllerTest.kt
index 555484c..8acde36 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickStatusBarHeaderControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QuickStatusBarHeaderControllerTest.kt
@@ -20,6 +20,7 @@
 import android.testing.AndroidTestingRunner
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.scene.shared.flag.FakeSceneContainerFlags
 import org.junit.After
 import org.junit.Before
 import org.junit.Test
@@ -42,6 +43,8 @@
     @Mock(answer = Answers.RETURNS_DEEP_STUBS)
     private lateinit var context: Context
 
+    private val sceneContainerFlags = FakeSceneContainerFlags()
+
     private lateinit var controller: QuickStatusBarHeaderController
 
     @Before
@@ -51,7 +54,11 @@
         `when`(view.isAttachedToWindow).thenReturn(true)
         `when`(view.context).thenReturn(context)
 
-        controller = QuickStatusBarHeaderController(view, quickQSPanelController)
+        controller = QuickStatusBarHeaderController(
+                view,
+                quickQSPanelController,
+                sceneContainerFlags,
+        )
     }
 
     @After
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt
new file mode 100644
index 0000000..c1c0126
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt
@@ -0,0 +1,294 @@
+/*
+ * 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.qs.ui.adapter
+
+import android.os.Bundle
+import android.view.View
+import androidx.asynclayoutinflater.view.AsyncLayoutInflater
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.qs.QSImpl
+import com.android.systemui.qs.dagger.QSComponent
+import com.android.systemui.qs.dagger.QSSceneComponent
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.capture
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.nullable
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import javax.inject.Provider
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.verify
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@OptIn(ExperimentalCoroutinesApi::class)
+class QSSceneAdapterImplTest : SysuiTestCase() {
+
+    private val testDispatcher = StandardTestDispatcher()
+    private val testScope = TestScope(testDispatcher)
+
+    private val qsImplProvider =
+        object : Provider<QSImpl> {
+            val impls = mutableListOf<QSImpl>()
+
+            override fun get(): QSImpl {
+                return mock<QSImpl> {
+                        lateinit var _view: View
+                        whenever(onComponentCreated(any(), any())).then {
+                            _view = it.getArgument<QSComponent>(0).getRootView()
+                            Unit
+                        }
+                        whenever(view).thenAnswer { _view }
+                    }
+                    .also { impls.add(it) }
+            }
+        }
+
+    private val qsSceneComponentFactory =
+        object : QSSceneComponent.Factory {
+            val components = mutableListOf<QSSceneComponent>()
+
+            override fun create(rootView: View): QSSceneComponent {
+                return mock<QSSceneComponent> { whenever(this.getRootView()).thenReturn(rootView) }
+                    .also { components.add(it) }
+            }
+        }
+
+    private val mockAsyncLayoutInflater =
+        mock<AsyncLayoutInflater>() {
+            whenever(inflate(anyInt(), nullable(), any())).then { invocation ->
+                val mockView = mock<View>()
+                invocation
+                    .getArgument<AsyncLayoutInflater.OnInflateFinishedListener>(2)
+                    .onInflateFinished(
+                        mockView,
+                        invocation.getArgument(0),
+                        invocation.getArgument(1),
+                    )
+            }
+        }
+
+    private val underTest =
+        QSSceneAdapterImpl(
+            qsSceneComponentFactory,
+            qsImplProvider,
+            testDispatcher,
+            testScope.backgroundScope,
+            { mockAsyncLayoutInflater },
+        )
+
+    @Test
+    fun inflate() =
+        testScope.runTest {
+            val qsImpl by collectLastValue(underTest.qsImpl)
+
+            assertThat(qsImpl).isNull()
+
+            underTest.inflate(context)
+            runCurrent()
+
+            assertThat(qsImpl).isNotNull()
+            assertThat(qsImpl).isSameInstanceAs(qsImplProvider.impls[0])
+            val inOrder = inOrder(qsImpl!!)
+            inOrder.verify(qsImpl!!).onCreate(nullable())
+            inOrder
+                .verify(qsImpl!!)
+                .onComponentCreated(
+                    eq(qsSceneComponentFactory.components[0]),
+                    any(),
+                )
+        }
+
+    @Test
+    fun initialState_closed() =
+        testScope.runTest {
+            val qsImpl by collectLastValue(underTest.qsImpl)
+
+            underTest.inflate(context)
+            runCurrent()
+
+            with(qsImpl!!) {
+                verify(this).setQsVisible(false)
+                verify(this)
+                    .setQsExpansion(
+                        /* expansion= */ 0f,
+                        /* panelExpansionFraction= */ 1f,
+                        /* proposedTranslation= */ 0f,
+                        /* squishinessFraction= */ 1f,
+                    )
+                verify(this).setListening(false)
+                verify(this).setExpanded(false)
+                verify(this)
+                    .setTransitionToFullShadeProgress(
+                        /* isTransitioningToFullShade= */ false,
+                        /* qsTransitionFraction= */ 1f,
+                        /* qsSquishinessFraction = */ 1f,
+                    )
+            }
+        }
+
+    @Test
+    fun state_qqs() =
+        testScope.runTest {
+            val qsImpl by collectLastValue(underTest.qsImpl)
+
+            underTest.inflate(context)
+            runCurrent()
+            clearInvocations(qsImpl!!)
+
+            underTest.setState(QSSceneAdapter.State.QQS)
+            with(qsImpl!!) {
+                verify(this).setQsVisible(true)
+                verify(this)
+                    .setQsExpansion(
+                        /* expansion= */ 0f,
+                        /* panelExpansionFraction= */ 1f,
+                        /* proposedTranslation= */ 0f,
+                        /* squishinessFraction= */ 1f,
+                    )
+                verify(this).setListening(true)
+                verify(this).setExpanded(true)
+                verify(this)
+                    .setTransitionToFullShadeProgress(
+                        /* isTransitioningToFullShade= */ false,
+                        /* qsTransitionFraction= */ 1f,
+                        /* qsSquishinessFraction = */ 1f,
+                    )
+            }
+        }
+
+    @Test
+    fun state_qs() =
+        testScope.runTest {
+            val qsImpl by collectLastValue(underTest.qsImpl)
+
+            underTest.inflate(context)
+            runCurrent()
+            clearInvocations(qsImpl!!)
+
+            underTest.setState(QSSceneAdapter.State.QS)
+            with(qsImpl!!) {
+                verify(this).setQsVisible(true)
+                verify(this)
+                    .setQsExpansion(
+                        /* expansion= */ 1f,
+                        /* panelExpansionFraction= */ 1f,
+                        /* proposedTranslation= */ 0f,
+                        /* squishinessFraction= */ 1f,
+                    )
+                verify(this).setListening(true)
+                verify(this).setExpanded(true)
+                verify(this)
+                    .setTransitionToFullShadeProgress(
+                        /* isTransitioningToFullShade= */ false,
+                        /* qsTransitionFraction= */ 1f,
+                        /* qsSquishinessFraction = */ 1f,
+                    )
+            }
+        }
+
+    @Test
+    fun customizing_QS() =
+        testScope.runTest {
+            val customizing by collectLastValue(underTest.isCustomizing)
+
+            underTest.inflate(context)
+            runCurrent()
+            underTest.setState(QSSceneAdapter.State.QS)
+
+            assertThat(customizing).isFalse()
+
+            underTest.setCustomizerShowing(true)
+            assertThat(customizing).isTrue()
+
+            underTest.setCustomizerShowing(false)
+            assertThat(customizing).isFalse()
+        }
+
+    @Test
+    fun customizing_moveToQQS_stopCustomizing() =
+        testScope.runTest {
+            val qsImpl by collectLastValue(underTest.qsImpl)
+
+            underTest.inflate(context)
+            runCurrent()
+            underTest.setState(QSSceneAdapter.State.QS)
+            underTest.setCustomizerShowing(true)
+
+            underTest.setState(QSSceneAdapter.State.QQS)
+            runCurrent()
+            verify(qsImpl!!).closeCustomizerImmediately()
+        }
+
+    @Test
+    fun customizing_moveToClosed_stopCustomizing() =
+        testScope.runTest {
+            val qsImpl by collectLastValue(underTest.qsImpl)
+
+            underTest.inflate(context)
+            runCurrent()
+            underTest.setState(QSSceneAdapter.State.QS)
+            underTest.setCustomizerShowing(true)
+            runCurrent()
+
+            underTest.setState(QSSceneAdapter.State.CLOSED)
+            verify(qsImpl!!).closeCustomizerImmediately()
+        }
+
+    @Test
+    fun reinflation_previousStateDestroyed() =
+        testScope.runTest {
+            val qsImpl by collectLastValue(underTest.qsImpl)
+
+            underTest.inflate(context)
+            runCurrent()
+            val oldQsImpl = qsImpl!!
+
+            underTest.inflate(context)
+            runCurrent()
+            val newQSImpl = qsImpl!!
+
+            assertThat(oldQsImpl).isNotSameInstanceAs(newQSImpl)
+            val inOrder = inOrder(oldQsImpl, newQSImpl)
+            val bundleArgCaptor = argumentCaptor<Bundle>()
+
+            inOrder.verify(oldQsImpl).onSaveInstanceState(capture(bundleArgCaptor))
+            inOrder.verify(oldQsImpl).onDestroy()
+            assertThat(newQSImpl).isSameInstanceAs(qsImplProvider.impls[1])
+            inOrder.verify(newQSImpl).onCreate(nullable())
+            inOrder
+                .verify(newQSImpl)
+                .onComponentCreated(
+                    qsSceneComponentFactory.components[1],
+                    bundleArgCaptor.value,
+                )
+        }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt
index d582b9e..ef129c8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt
@@ -23,9 +23,12 @@
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.flags.FakeFeatureFlagsClassic
 import com.android.systemui.flags.Flags
+import com.android.systemui.qs.ui.adapter.FakeQSSceneAdapter
 import com.android.systemui.scene.SceneTestUtils
+import com.android.systemui.scene.shared.model.Direction
 import com.android.systemui.scene.shared.model.SceneKey
 import com.android.systemui.scene.shared.model.SceneModel
+import com.android.systemui.scene.shared.model.UserAction
 import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel
 import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository
 import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor
@@ -53,6 +56,7 @@
     private val sceneInteractor = utils.sceneInteractor()
     private val mobileIconsInteractor = FakeMobileIconsInteractor(FakeMobileMappingsProxy(), mock())
     private val flags = FakeFeatureFlagsClassic().also { it.set(Flags.NEW_NETWORK_SLICE_UI, false) }
+    private val qsFlexiglassAdapter = FakeQSSceneAdapter { _, _ -> mock() }
 
     private var mobileIconsViewModel: MobileIconsViewModel =
         MobileIconsViewModel(
@@ -96,6 +100,7 @@
                         sceneInteractor = sceneInteractor,
                     ),
                 shadeHeaderViewModel = shadeHeaderViewModel,
+                qsSceneAdapter = qsFlexiglassAdapter,
             )
     }
 
@@ -124,4 +129,33 @@
 
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
         }
+
+    @Test
+    fun destinationsNotCustomizing() =
+        testScope.runTest {
+            val destinations by collectLastValue(underTest.destinationScenes)
+            qsFlexiglassAdapter.setCustomizing(false)
+
+            assertThat(destinations)
+                .isEqualTo(
+                    mapOf(
+                        UserAction.Back to SceneModel(SceneKey.Shade),
+                        UserAction.Swipe(Direction.UP) to SceneModel(SceneKey.Shade),
+                    )
+                )
+        }
+
+    @Test
+    fun destinationsCustomizing() =
+        testScope.runTest {
+            val destinations by collectLastValue(underTest.destinationScenes)
+            qsFlexiglassAdapter.setCustomizing(true)
+
+            assertThat(destinations)
+                .isEqualTo(
+                    mapOf(
+                        UserAction.Back to SceneModel(SceneKey.QuickSettings),
+                    )
+                )
+        }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
index d1db9c1..cef888b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
@@ -20,6 +20,7 @@
 
 import android.telecom.TelecomManager
 import android.telephony.TelephonyManager
+import android.view.View
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.internal.R
@@ -39,6 +40,7 @@
 import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest
 import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest
 import com.android.systemui.power.domain.interactor.PowerInteractorFactory
+import com.android.systemui.qs.ui.adapter.FakeQSSceneAdapter
 import com.android.systemui.scene.domain.startable.SceneContainerStartable
 import com.android.systemui.scene.shared.model.ObservableTransitionState
 import com.android.systemui.scene.shared.model.SceneKey
@@ -182,6 +184,8 @@
 
     private var bouncerSceneJob: Job? = null
 
+    private val qsFlexiglassAdapter = FakeQSSceneAdapter(inflateDelegate = { _, _ -> mock<View>() })
+
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
@@ -232,6 +236,7 @@
                 applicationScope = testScope.backgroundScope,
                 deviceEntryInteractor = deviceEntryInteractor,
                 shadeHeaderViewModel = shadeHeaderViewModel,
+                qsSceneAdapter = qsFlexiglassAdapter,
             )
 
         utils.deviceEntryRepository.setUnlocked(false)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
index fa849fe..0d3b2b3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
@@ -23,6 +23,7 @@
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.flags.FakeFeatureFlagsClassic
 import com.android.systemui.flags.Flags
+import com.android.systemui.qs.ui.adapter.FakeQSSceneAdapter
 import com.android.systemui.scene.SceneTestUtils
 import com.android.systemui.scene.shared.model.SceneKey
 import com.android.systemui.scene.shared.model.SceneModel
@@ -76,6 +77,8 @@
             scope = testScope.backgroundScope,
         )
 
+    private val qsFlexiglassAdapter = FakeQSSceneAdapter { _, _ -> mock() }
+
     private lateinit var shadeHeaderViewModel: ShadeHeaderViewModel
 
     private lateinit var underTest: ShadeSceneViewModel
@@ -97,6 +100,7 @@
                 applicationScope = testScope.backgroundScope,
                 deviceEntryInteractor = deviceEntryInteractor,
                 shadeHeaderViewModel = shadeHeaderViewModel,
+                qsSceneAdapter = qsFlexiglassAdapter,
             )
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
index 86a5c52..027c11c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
@@ -121,6 +121,8 @@
 import com.android.systemui.power.domain.interactor.PowerInteractor;
 import com.android.systemui.res.R;
 import com.android.systemui.scene.domain.interactor.WindowRootViewVisibilityInteractor;
+import com.android.systemui.scene.shared.flag.FakeSceneContainerFlags;
+import com.android.systemui.scene.shared.flag.SceneContainerFlags;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.settings.brightness.BrightnessSliderController;
 import com.android.systemui.shade.CameraLauncher;
@@ -329,6 +331,8 @@
     private final DumpManager mDumpManager = new DumpManager();
     private final ScreenLifecycle mScreenLifecycle = new ScreenLifecycle(mDumpManager);
 
+    private final SceneContainerFlags mSceneContainerFlags = new FakeSceneContainerFlags();
+
     @Before
     public void setup() throws Exception {
         MockitoAnnotations.initMocks(this);
@@ -555,7 +559,8 @@
                 mAlternateBouncerInteractor,
                 mUserTracker,
                 () -> mFingerprintManager,
-                mActivityStarter
+                mActivityStarter,
+                mSceneContainerFlags
         );
         mScreenLifecycle.addObserver(mCentralSurfaces.mScreenObserver);
         mCentralSurfaces.initShadeVisibilityListener();
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/adapter/FakeQSSceneAdapter.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/adapter/FakeQSSceneAdapter.kt
new file mode 100644
index 0000000..2902c3f
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/adapter/FakeQSSceneAdapter.kt
@@ -0,0 +1,54 @@
+/*
+ * 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.qs.ui.adapter
+
+import android.content.Context
+import android.view.View
+import android.view.ViewGroup
+import com.android.systemui.qs.ui.adapter.QSSceneAdapter
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.filterNotNull
+
+class FakeQSSceneAdapter(
+    private val inflateDelegate: suspend (Context, ViewGroup?) -> View,
+) : QSSceneAdapter {
+    private val _customizing = MutableStateFlow(false)
+    override val isCustomizing: StateFlow<Boolean> = _customizing.asStateFlow()
+
+    private val _view = MutableStateFlow<View?>(null)
+    override val qsView: Flow<View> = _view.filterNotNull()
+
+    private val _state = MutableStateFlow<QSSceneAdapter.State?>(null)
+    val state = _state.filterNotNull()
+
+    override suspend fun inflate(context: Context, parent: ViewGroup?) {
+        _view.value = inflateDelegate(context, parent)
+    }
+
+    override fun setState(state: QSSceneAdapter.State) {
+        if (_view.value != null) {
+            _state.value = state
+        }
+    }
+
+    fun setCustomizing(value: Boolean) {
+        _customizing.value = value
+    }
+}