Create ViewModel for QS compose and a basic fragment
This is the first iteration of integrating the new QS into compose. It
achieves the following:
* Fragment integrated into NotificationShade
* Shows QQS when collapsed and QS when expanded
* Has all elements (tiles, brightness, footer, edit mode).
* Tiles and footer are clickable. Long click on tiles is broken
(b/358376794).
* Tests that show qqs/qs pass.
This creates a skeleton viewmodel to which calls will be forwarded from
the fragment (QS interface) that come from NPVC. That way it's
transparent for the current NPVC.
Missing:
* Animations and transitions
* Most calls into QS are Noop.
* No final UI (as that will be handled in the components).
Test: atest QSFragmentComposeViewModelTest
Test: atest PlatformScenarioTests
Bug: 353254347
Bug: 353253277
Bug: 353254131
Flag: com.android.systemui.qs_ui_refactor_compose_fragment
Change-Id: I44d835473591f4865696d6941c4b9223466e5aa3
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index 6d78705..5ea75be 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -748,6 +748,7 @@
"//frameworks/libs/systemui:motion_tool_lib",
"//frameworks/libs/systemui:contextualeducationlib",
"androidx.core_core-animation-testing",
+ "androidx.lifecycle_lifecycle-runtime-testing",
"androidx.compose.ui_ui",
"flag-junit",
"ravenwood-junit",
@@ -789,6 +790,7 @@
"SystemUI-tests-base",
"androidx.test.uiautomator_uiautomator",
"androidx.core_core-animation-testing",
+ "androidx.lifecycle_lifecycle-runtime-testing",
"mockito-target-extended-minus-junit4",
"mockito-kotlin-nodeps",
"androidx.test.ext.junit",
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeScene.kt
index eea00c4..fb7c422 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeScene.kt
@@ -29,8 +29,6 @@
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
-import androidx.compose.material3.Button
-import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
@@ -42,6 +40,7 @@
import com.android.compose.animation.scene.UserActionResult
import com.android.systemui.battery.BatteryMeterViewController
import com.android.systemui.brightness.ui.compose.BrightnessSliderContainer
+import com.android.systemui.compose.modifiers.sysuiResTag
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.ui.composable.LockscreenContent
import com.android.systemui.lifecycle.rememberViewModel
@@ -114,7 +113,7 @@
}
@Composable
-private fun ShadeBody(
+fun ShadeBody(
viewModel: QuickSettingsContainerViewModel,
) {
val isEditing by viewModel.editModeViewModel.isEditing.collectAsStateWithLifecycle()
@@ -131,6 +130,7 @@
} else {
QuickSettingsLayout(
viewModel = viewModel,
+ modifier = Modifier.sysuiResTag("quick_settings_panel")
)
}
}
@@ -158,11 +158,6 @@
Modifier.fillMaxWidth().heightIn(max = QuickSettingsShade.Dimensions.GridMaxHeight),
viewModel.editModeViewModel::startEditing,
)
- Button(
- onClick = { viewModel.editModeViewModel.startEditing() },
- ) {
- Text("Edit mode")
- }
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt
new file mode 100644
index 0000000..5999265
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.composefragment.viewmodel
+
+import android.content.testableContext
+import android.testing.TestableLooper.RunWithLooper
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.testing.TestLifecycleOwner
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.qs.fgsManagerController
+import com.android.systemui.res.R
+import com.android.systemui.shade.largeScreenHeaderHelper
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestResult
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.test.setMain
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@RunWithLooper
+@OptIn(ExperimentalCoroutinesApi::class)
+class QSFragmentComposeViewModelTest : SysuiTestCase() {
+ private val kosmos = testKosmos()
+
+ private val lifecycleOwner =
+ TestLifecycleOwner(
+ initialState = Lifecycle.State.CREATED,
+ coroutineDispatcher = kosmos.testDispatcher,
+ )
+
+ private val underTest by lazy {
+ kosmos.qsFragmentComposeViewModelFactory.create(lifecycleOwner.lifecycleScope)
+ }
+
+ @Before
+ fun setUp() {
+ Dispatchers.setMain(kosmos.testDispatcher)
+ }
+
+ @After
+ fun teardown() {
+ Dispatchers.resetMain()
+ }
+
+ // For now the state changes at 0.5f expansion. This will change once we implement animation
+ // (and this test will fail)
+ @Test
+ fun qsExpansionValueChanges_correctExpansionState() =
+ with(kosmos) {
+ testScope.testWithinLifecycle {
+ val expansionState by collectLastValue(underTest.expansionState)
+
+ underTest.qsExpansionValue = 0f
+ assertThat(expansionState)
+ .isEqualTo(QSFragmentComposeViewModel.QSExpansionState.QQS)
+
+ underTest.qsExpansionValue = 0.3f
+ assertThat(expansionState)
+ .isEqualTo(QSFragmentComposeViewModel.QSExpansionState.QQS)
+
+ underTest.qsExpansionValue = 0.7f
+ assertThat(expansionState).isEqualTo(QSFragmentComposeViewModel.QSExpansionState.QS)
+
+ underTest.qsExpansionValue = 1f
+ assertThat(expansionState).isEqualTo(QSFragmentComposeViewModel.QSExpansionState.QS)
+ }
+ }
+
+ @Test
+ fun qqsHeaderHeight_largeScreenHeader_0() =
+ with(kosmos) {
+ testScope.testWithinLifecycle {
+ val qqsHeaderHeight by collectLastValue(underTest.qqsHeaderHeight)
+
+ testableContext.orCreateTestableResources.addOverride(
+ R.bool.config_use_large_screen_shade_header,
+ true
+ )
+ fakeConfigurationRepository.onConfigurationChange()
+
+ assertThat(qqsHeaderHeight).isEqualTo(0)
+ }
+ }
+
+ @Test
+ fun qqsHeaderHeight_noLargeScreenHeader_providedByHelper() =
+ with(kosmos) {
+ testScope.testWithinLifecycle {
+ val qqsHeaderHeight by collectLastValue(underTest.qqsHeaderHeight)
+
+ testableContext.orCreateTestableResources.addOverride(
+ R.bool.config_use_large_screen_shade_header,
+ false
+ )
+ fakeConfigurationRepository.onConfigurationChange()
+
+ assertThat(qqsHeaderHeight)
+ .isEqualTo(largeScreenHeaderHelper.getLargeScreenHeaderHeight())
+ }
+ }
+
+ @Test
+ fun footerActionsControllerInit() =
+ with(kosmos) {
+ testScope.testWithinLifecycle {
+ underTest
+ runCurrent()
+ assertThat(fgsManagerController.initialized).isTrue()
+ }
+ }
+
+ private inline fun TestScope.testWithinLifecycle(
+ crossinline block: suspend TestScope.() -> TestResult
+ ): TestResult {
+ return runTest {
+ lifecycleOwner.setCurrentState(Lifecycle.State.RESUMED)
+ block().also { lifecycleOwner.setCurrentState(Lifecycle.State.DESTROYED) }
+ }
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneContentViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneContentViewModelTest.kt
index 9563538..1118a61 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneContentViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneContentViewModelTest.kt
@@ -17,6 +17,7 @@
package com.android.systemui.qs.ui.viewmodel
import android.testing.TestableLooper.RunWithLooper
+import androidx.lifecycle.LifecycleOwner
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
@@ -59,7 +60,7 @@
private val footerActionsViewModel = mock<FooterActionsViewModel>()
private val footerActionsViewModelFactory =
mock<FooterActionsViewModel.Factory> {
- whenever(create(any())).thenReturn(footerActionsViewModel)
+ whenever(create(any<LifecycleOwner>())).thenReturn(footerActionsViewModel)
}
private val footerActionsController = mock<FooterActionsController>()
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragmentStartable.kt b/packages/SystemUI/src/com/android/systemui/qs/QSFragmentStartable.kt
index 9fa6769..bb238f2 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSFragmentStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSFragmentStartable.kt
@@ -19,6 +19,7 @@
import com.android.systemui.CoreStartable
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.fragments.FragmentService
+import com.android.systemui.qs.composefragment.QSFragmentCompose
import dagger.Binds
import dagger.Module
import dagger.multibindings.ClassKey
@@ -31,13 +32,18 @@
@Inject
constructor(
private val fragmentService: FragmentService,
- private val qsFragmentLegacyProvider: Provider<QSFragmentLegacy>
+ private val qsFragmentLegacyProvider: Provider<QSFragmentLegacy>,
+ private val qsFragmentComposeProvider: Provider<QSFragmentCompose>,
) : CoreStartable {
override fun start() {
fragmentService.addFragmentInstantiationProvider(
QSFragmentLegacy::class.java,
qsFragmentLegacyProvider
)
+ fragmentService.addFragmentInstantiationProvider(
+ QSFragmentCompose::class.java,
+ qsFragmentComposeProvider
+ )
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
new file mode 100644
index 0000000..5d81d4f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
@@ -0,0 +1,442 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.composefragment
+
+import android.annotation.SuppressLint
+import android.graphics.Rect
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.activity.OnBackPressedDispatcher
+import androidx.activity.OnBackPressedDispatcherOwner
+import androidx.activity.setViewTreeOnBackPressedDispatcherOwner
+import androidx.compose.animation.AnimatedContent
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.navigationBars
+import androidx.compose.foundation.layout.windowInsetsPadding
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.layout
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.layout.positionInRoot
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.unit.round
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.compose.modifiers.height
+import com.android.compose.modifiers.padding
+import com.android.compose.theme.PlatformTheme
+import com.android.systemui.compose.modifiers.sysuiResTag
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.plugins.qs.QS
+import com.android.systemui.plugins.qs.QSContainerController
+import com.android.systemui.qs.composefragment.viewmodel.QSFragmentComposeViewModel
+import com.android.systemui.qs.flags.QSComposeFragment
+import com.android.systemui.qs.footer.ui.compose.FooterActions
+import com.android.systemui.qs.panels.ui.compose.QuickQuickSettings
+import com.android.systemui.qs.ui.composable.QuickSettingsTheme
+import com.android.systemui.qs.ui.composable.ShadeBody
+import com.android.systemui.res.R
+import com.android.systemui.util.LifecycleFragment
+import java.util.function.Consumer
+import javax.inject.Inject
+import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.launch
+
+@SuppressLint("ValidFragment")
+class QSFragmentCompose
+@Inject
+constructor(
+ private val qsFragmentComposeViewModelFactory: QSFragmentComposeViewModel.Factory,
+) : LifecycleFragment(), QS {
+
+ private val scrollListener = MutableStateFlow<QS.ScrollListener?>(null)
+ private val heightListener = MutableStateFlow<QS.HeightListener?>(null)
+ private val qsContainerController = MutableStateFlow<QSContainerController?>(null)
+
+ private lateinit var viewModel: QSFragmentComposeViewModel
+
+ // Starting with a non-zero value makes it so that it has a non-zero height on first expansion
+ // This is important for `QuickSettingsControllerImpl.mMinExpansionHeight` to detect a "change".
+ private val qqsHeight = MutableStateFlow(1)
+ private val qsHeight = MutableStateFlow(0)
+ private val qqsVisible = MutableStateFlow(false)
+ private val qqsPositionOnRoot = Rect()
+ private val composeViewPositionOnScreen = Rect()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ QSComposeFragment.isUnexpectedlyInLegacyMode()
+ viewModel = qsFragmentComposeViewModelFactory.create(lifecycleScope)
+
+ setListenerCollections()
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ val context = inflater.context
+ return ComposeView(context).apply {
+ setBackPressedDispatcher()
+ setContent {
+ PlatformTheme {
+ val visible by viewModel.qsVisible.collectAsStateWithLifecycle()
+ val qsState by viewModel.expansionState.collectAsStateWithLifecycle()
+
+ AnimatedVisibility(
+ visible = visible,
+ modifier = Modifier.windowInsetsPadding(WindowInsets.navigationBars)
+ ) {
+ AnimatedContent(targetState = qsState) {
+ when (it) {
+ QSFragmentComposeViewModel.QSExpansionState.QQS -> {
+ QuickQuickSettingsElement()
+ }
+ QSFragmentComposeViewModel.QSExpansionState.QS -> {
+ QuickSettingsElement()
+ }
+ else -> {}
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ override fun setPanelView(notificationPanelView: QS.HeightListener?) {
+ heightListener.value = notificationPanelView
+ }
+
+ override fun hideImmediately() {
+ // view?.animate()?.cancel()
+ // view?.y = -qsMinExpansionHeight.toFloat()
+ }
+
+ override fun getQsMinExpansionHeight(): Int {
+ // TODO (b/353253277) implement split screen
+ return qqsHeight.value
+ }
+
+ override fun getDesiredHeight(): Int {
+ /*
+ * Looking at the code, it seems that
+ * * If customizing, then the height is that of the view post-layout, which is set by
+ * QSContainerImpl.calculateContainerHeight, which is the height the customizer takes
+ * * If not customizing, it's the measured height. So we may want to surface that.
+ */
+ return view?.height ?: 0
+ }
+
+ override fun setHeightOverride(desiredHeight: Int) {
+ viewModel.heightOverrideValue = desiredHeight
+ }
+
+ override fun setHeaderClickable(qsExpansionEnabled: Boolean) {
+ // Empty method
+ }
+
+ override fun isCustomizing(): Boolean {
+ return viewModel.containerViewModel.editModeViewModel.isEditing.value
+ }
+
+ override fun closeCustomizer() {
+ viewModel.containerViewModel.editModeViewModel.stopEditing()
+ }
+
+ override fun setOverscrolling(overscrolling: Boolean) {
+ viewModel.stackScrollerOverscrollingValue = overscrolling
+ }
+
+ override fun setExpanded(qsExpanded: Boolean) {
+ viewModel.isQSExpanded = qsExpanded
+ }
+
+ override fun setListening(listening: Boolean) {
+ // Not needed, views start listening and collection when composed
+ }
+
+ override fun setQsVisible(qsVisible: Boolean) {
+ viewModel.isQSVisible = qsVisible
+ }
+
+ override fun isShowingDetail(): Boolean {
+ return isCustomizing
+ }
+
+ override fun closeDetail() {
+ closeCustomizer()
+ }
+
+ override fun animateHeaderSlidingOut() {
+ // TODO(b/353254353)
+ }
+
+ override fun setQsExpansion(
+ qsExpansionFraction: Float,
+ panelExpansionFraction: Float,
+ headerTranslation: Float,
+ squishinessFraction: Float
+ ) {
+ viewModel.qsExpansionValue = qsExpansionFraction
+ viewModel.panelExpansionFractionValue = panelExpansionFraction
+ viewModel.squishinessFractionValue = squishinessFraction
+
+ // TODO(b/353254353) Handle header translation
+ }
+
+ override fun setHeaderListening(listening: Boolean) {
+ // Not needed, header will start listening as soon as it's composed
+ }
+
+ override fun notifyCustomizeChanged() {
+ // Not needed, only called from inside customizer
+ }
+
+ override fun setContainerController(controller: QSContainerController?) {
+ qsContainerController.value = controller
+ }
+
+ override fun setCollapseExpandAction(action: Runnable?) {
+ // Nothing to do yet. But this should be wired to a11y
+ }
+
+ override fun getHeightDiff(): Int {
+ return 0 // For now TODO(b/353254353)
+ }
+
+ override fun getHeader(): View? {
+ QSComposeFragment.isUnexpectedlyInLegacyMode()
+ return null
+ }
+
+ override fun setShouldUpdateSquishinessOnMedia(shouldUpdate: Boolean) {
+ super.setShouldUpdateSquishinessOnMedia(shouldUpdate)
+ // TODO (b/353253280)
+ }
+
+ override fun setInSplitShade(shouldTranslate: Boolean) {
+ // TODO (b/356435605)
+ }
+
+ override fun setTransitionToFullShadeProgress(
+ isTransitioningToFullShade: Boolean,
+ qsTransitionFraction: Float,
+ qsSquishinessFraction: Float
+ ) {
+ super.setTransitionToFullShadeProgress(
+ isTransitioningToFullShade,
+ qsTransitionFraction,
+ qsSquishinessFraction
+ )
+ }
+
+ override fun setFancyClipping(
+ leftInset: Int,
+ top: Int,
+ rightInset: Int,
+ bottom: Int,
+ cornerRadius: Int,
+ visible: Boolean,
+ fullWidth: Boolean
+ ) {}
+
+ override fun isFullyCollapsed(): Boolean {
+ return !viewModel.isQSVisible
+ }
+
+ override fun setCollapsedMediaVisibilityChangedListener(listener: Consumer<Boolean>?) {
+ // TODO (b/353253280)
+ }
+
+ override fun setScrollListener(scrollListener: QS.ScrollListener?) {
+ this.scrollListener.value = scrollListener
+ }
+
+ override fun setOverScrollAmount(overScrollAmount: Int) {
+ super.setOverScrollAmount(overScrollAmount)
+ }
+
+ override fun setIsNotificationPanelFullWidth(isFullWidth: Boolean) {
+ viewModel.isSmallScreenValue = isFullWidth
+ }
+
+ override fun getHeaderTop(): Int {
+ return viewModel.qqsHeaderHeight.value
+ }
+
+ override fun getHeaderBottom(): Int {
+ return headerTop + qqsHeight.value
+ }
+
+ override fun getHeaderLeft(): Int {
+ return qqsPositionOnRoot.left
+ }
+
+ override fun getHeaderBoundsOnScreen(outBounds: Rect) {
+ outBounds.set(qqsPositionOnRoot)
+ view?.getBoundsOnScreen(composeViewPositionOnScreen)
+ ?: run { composeViewPositionOnScreen.setEmpty() }
+ qqsPositionOnRoot.offset(composeViewPositionOnScreen.left, composeViewPositionOnScreen.top)
+ }
+
+ override fun isHeaderShown(): Boolean {
+ return qqsVisible.value
+ }
+
+ private fun setListenerCollections() {
+ lifecycleScope.launch {
+ lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ launch {
+ // TODO
+ // setListenerJob(
+ // scrollListener,
+ //
+ // )
+ }
+ launch {
+ setListenerJob(
+ heightListener,
+ viewModel.containerViewModel.editModeViewModel.isEditing
+ ) {
+ onQsHeightChanged()
+ }
+ }
+ launch {
+ setListenerJob(
+ qsContainerController,
+ viewModel.containerViewModel.editModeViewModel.isEditing
+ ) {
+ setCustomizerShowing(it)
+ }
+ }
+ }
+ }
+ }
+
+ @Composable
+ private fun QuickQuickSettingsElement() {
+ val qqsPadding by viewModel.qqsHeaderHeight.collectAsStateWithLifecycle()
+ DisposableEffect(Unit) {
+ qqsVisible.value = true
+
+ onDispose { qqsVisible.value = false }
+ }
+ Column(modifier = Modifier.sysuiResTag("quick_qs_panel")) {
+ QuickQuickSettings(
+ viewModel = viewModel.containerViewModel.quickQuickSettingsViewModel,
+ modifier =
+ Modifier.onGloballyPositioned { coordinates ->
+ val (leftFromRoot, topFromRoot) = coordinates.positionInRoot().round()
+ val (width, height) = coordinates.size
+ qqsPositionOnRoot.set(
+ leftFromRoot,
+ topFromRoot,
+ leftFromRoot + width,
+ topFromRoot + height
+ )
+ }
+ .layout { measurable, constraints ->
+ val placeable = measurable.measure(constraints)
+ qqsHeight.value = placeable.height
+
+ layout(placeable.width, placeable.height) { placeable.place(0, 0) }
+ }
+ .padding(top = { qqsPadding })
+ )
+ Spacer(modifier = Modifier.weight(1f))
+ }
+ }
+
+ @Composable
+ private fun QuickSettingsElement() {
+ val qqsPadding by viewModel.qqsHeaderHeight.collectAsStateWithLifecycle()
+ val qsExtraPadding = dimensionResource(R.dimen.qs_panel_padding_top)
+ Column {
+ Box(modifier = Modifier.fillMaxSize().weight(1f)) {
+ Column {
+ Spacer(modifier = Modifier.height { qqsPadding + qsExtraPadding.roundToPx() })
+ ShadeBody(viewModel = viewModel.containerViewModel)
+ }
+ }
+ QuickSettingsTheme {
+ FooterActions(
+ viewModel = viewModel.footerActionsViewModel,
+ qsVisibilityLifecycleOwner = this@QSFragmentCompose,
+ modifier = Modifier.sysuiResTag("qs_footer_actions")
+ )
+ }
+ }
+ }
+}
+
+private fun View.setBackPressedDispatcher() {
+ repeatWhenAttached {
+ repeatOnLifecycle(Lifecycle.State.CREATED) {
+ setViewTreeOnBackPressedDispatcherOwner(
+ object : OnBackPressedDispatcherOwner {
+ override val onBackPressedDispatcher =
+ OnBackPressedDispatcher().apply {
+ setOnBackInvokedDispatcher(it.viewRootImpl.onBackInvokedDispatcher)
+ }
+
+ override val lifecycle: Lifecycle = this@repeatWhenAttached.lifecycle
+ }
+ )
+ }
+ }
+}
+
+private suspend inline fun <Listener : Any, Data> setListenerJob(
+ listenerFlow: MutableStateFlow<Listener?>,
+ dataFlow: Flow<Data>,
+ crossinline onCollect: suspend Listener.(Data) -> Unit
+) {
+ coroutineScope {
+ try {
+ listenerFlow.collectLatest { listenerOrNull ->
+ listenerOrNull?.let { currentListener ->
+ launch {
+ // Called when editing mode changes
+ dataFlow.collect { currentListener.onCollect(it) }
+ }
+ }
+ }
+ awaitCancellation()
+ } finally {
+ listenerFlow.value = null
+ }
+ }
+}
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
new file mode 100644
index 0000000..9e109e4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt
@@ -0,0 +1,196 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.composefragment.viewmodel
+
+import android.content.res.Resources
+import android.graphics.Rect
+import androidx.lifecycle.LifecycleCoroutineScope
+import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.qs.FooterActionsController
+import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
+import com.android.systemui.qs.ui.viewmodel.QuickSettingsContainerViewModel
+import com.android.systemui.shade.LargeScreenHeaderHelper
+import com.android.systemui.shade.transition.LargeScreenShadeInterpolator
+import com.android.systemui.statusbar.SysuiStatusBarStateController
+import com.android.systemui.statusbar.disableflags.data.repository.DisableFlagsRepository
+import com.android.systemui.statusbar.phone.KeyguardBypassController
+import com.android.systemui.util.LargeScreenUtils
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
+
+class QSFragmentComposeViewModel
+@AssistedInject
+constructor(
+ val containerViewModel: QuickSettingsContainerViewModel,
+ @Main private val resources: Resources,
+ private val footerActionsViewModelFactory: FooterActionsViewModel.Factory,
+ private val footerActionsController: FooterActionsController,
+ private val sysuiStatusBarStateController: SysuiStatusBarStateController,
+ private val keyguardBypassController: KeyguardBypassController,
+ private val disableFlagsRepository: DisableFlagsRepository,
+ private val largeScreenShadeInterpolator: LargeScreenShadeInterpolator,
+ private val configurationInteractor: ConfigurationInteractor,
+ private val largeScreenHeaderHelper: LargeScreenHeaderHelper,
+ @Assisted private val lifecycleScope: LifecycleCoroutineScope,
+) {
+ val footerActionsViewModel =
+ footerActionsViewModelFactory.create(lifecycleScope).also {
+ lifecycleScope.launch { footerActionsController.init() }
+ }
+
+ private val _qsBounds = MutableStateFlow(Rect())
+
+ private val _qsExpanded = MutableStateFlow(false)
+ var isQSExpanded: Boolean
+ get() = _qsExpanded.value
+ set(value) {
+ _qsExpanded.value = value
+ }
+
+ private val _qsVisible = MutableStateFlow(false)
+ val qsVisible = _qsVisible.asStateFlow()
+ var isQSVisible: Boolean
+ get() = qsVisible.value
+ set(value) {
+ _qsVisible.value = value
+ }
+
+ private val _qsExpansion = MutableStateFlow(0f)
+ var qsExpansionValue: Float
+ get() = _qsExpansion.value
+ set(value) {
+ _qsExpansion.value = value
+ }
+
+ private val _panelFraction = MutableStateFlow(0f)
+ var panelExpansionFractionValue: Float
+ get() = _panelFraction.value
+ set(value) {
+ _panelFraction.value = value
+ }
+
+ private val _squishinessFraction = MutableStateFlow(0f)
+ var squishinessFractionValue: Float
+ get() = _squishinessFraction.value
+ set(value) {
+ _squishinessFraction.value = value
+ }
+
+ val qqsHeaderHeight =
+ configurationInteractor.onAnyConfigurationChange
+ .map {
+ if (LargeScreenUtils.shouldUseLargeScreenShadeHeader(resources)) {
+ 0
+ } else {
+ largeScreenHeaderHelper.getLargeScreenHeaderHeight()
+ }
+ }
+ .stateIn(lifecycleScope, SharingStarted.WhileSubscribed(), 0)
+
+ private val _headerAnimating = MutableStateFlow(false)
+
+ private val _stackScrollerOverscrolling = MutableStateFlow(false)
+ var stackScrollerOverscrollingValue: Boolean
+ get() = _stackScrollerOverscrolling.value
+ set(value) {
+ _stackScrollerOverscrolling.value = value
+ }
+
+ private val qsDisabled =
+ disableFlagsRepository.disableFlags
+ .map { !it.isQuickSettingsEnabled() }
+ .stateIn(
+ lifecycleScope,
+ SharingStarted.WhileSubscribed(),
+ !disableFlagsRepository.disableFlags.value.isQuickSettingsEnabled()
+ )
+
+ private val _showCollapsedOnKeyguard = MutableStateFlow(false)
+
+ private val _keyguardAndExpanded = MutableStateFlow(false)
+
+ private val _statusBarState = MutableStateFlow(-1)
+
+ private val _viewHeight = MutableStateFlow(0)
+
+ private val _headerTranslation = MutableStateFlow(0f)
+
+ private val _inSplitShade = MutableStateFlow(false)
+
+ private val _transitioningToFullShade = MutableStateFlow(false)
+
+ private val _lockscreenToShadeProgress = MutableStateFlow(false)
+
+ private val _overscrolling = MutableStateFlow(false)
+
+ private val _isSmallScreen = MutableStateFlow(false)
+ var isSmallScreenValue: Boolean
+ get() = _isSmallScreen.value
+ set(value) {
+ _isSmallScreen.value = value
+ }
+
+ private val _shouldUpdateMediaSquishiness = MutableStateFlow(false)
+
+ private val _heightOverride = MutableStateFlow(-1)
+ val heightOverride = _heightOverride.asStateFlow()
+ var heightOverrideValue: Int
+ get() = heightOverride.value
+ set(value) {
+ _heightOverride.value = value
+ }
+
+ val expansionState: StateFlow<QSExpansionState> =
+ combine(
+ _stackScrollerOverscrolling,
+ _qsExpanded,
+ _qsExpansion,
+ ) { args: Array<Any> ->
+ val expansion = args[2] as Float
+ if (expansion > 0.5f) {
+ QSExpansionState.QS
+ } else {
+ QSExpansionState.QQS
+ }
+ }
+ .stateIn(lifecycleScope, SharingStarted.WhileSubscribed(), QSExpansionState.QQS)
+
+ @AssistedFactory
+ interface Factory {
+ fun create(lifecycleScope: LifecycleCoroutineScope): QSFragmentComposeViewModel
+ }
+
+ sealed interface QSExpansionState {
+ data object QQS : QSExpansionState
+
+ data object QS : QSExpansionState
+
+ @JvmInline value class Expanding(val progress: Float) : QSExpansionState
+
+ @JvmInline value class Collapsing(val progress: Float) : QSExpansionState
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt
index ba45d17..6dc101a 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt
@@ -21,6 +21,7 @@
import android.view.ContextThemeWrapper
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleCoroutineScope
import androidx.lifecycle.LifecycleOwner
import com.android.settingslib.Utils
import com.android.systemui.animation.Expandable
@@ -41,6 +42,7 @@
import javax.inject.Named
import javax.inject.Provider
import kotlin.math.max
+import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -48,6 +50,8 @@
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.isActive
+import kotlinx.coroutines.launch
private const val TAG = "FooterActionsViewModel"
@@ -140,6 +144,30 @@
showPowerButton,
)
}
+
+ fun create(lifecycleCoroutineScope: LifecycleCoroutineScope): FooterActionsViewModel {
+ val globalActionsDialogLite = globalActionsDialogLiteProvider.get()
+ if (lifecycleCoroutineScope.isActive) {
+ lifecycleCoroutineScope.launch {
+ try {
+ awaitCancellation()
+ } finally {
+ globalActionsDialogLite.destroy()
+ }
+ }
+ } else {
+ globalActionsDialogLite.destroy()
+ }
+
+ return FooterActionsViewModel(
+ context,
+ footerActionsInteractor,
+ falsingManager,
+ globalActionsDialogLite,
+ activityStarter,
+ showPowerButton,
+ )
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt
index 2ee957e..08a56bf 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt
@@ -39,6 +39,7 @@
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.android.systemui.compose.modifiers.sysuiResTag
import com.android.systemui.qs.panels.dagger.PaginatedBaseLayoutType
import com.android.systemui.qs.panels.ui.compose.PaginatedGridLayout.Dimensions.FooterHeight
import com.android.systemui.qs.panels.ui.compose.PaginatedGridLayout.Dimensions.InterPageSpacing
@@ -77,7 +78,7 @@
Column {
HorizontalPager(
state = pagerState,
- modifier = Modifier,
+ modifier = Modifier.sysuiResTag("qs_pager"),
pageSpacing = if (pages.size > 1) InterPageSpacing else 0.dp,
beyondViewportPageCount = 1,
verticalAlignment = Alignment.Top,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt
index af3803b..a9027ff 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt
@@ -25,6 +25,7 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.dimensionResource
import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.android.systemui.compose.modifiers.sysuiResTag
import com.android.systemui.qs.panels.ui.viewmodel.QuickQuickSettingsViewModel
import com.android.systemui.res.R
@@ -44,7 +45,10 @@
}
val columns by viewModel.columns.collectAsStateWithLifecycle()
- TileLazyGrid(modifier = modifier, columns = GridCells.Fixed(columns)) {
+ TileLazyGrid(
+ modifier = modifier.sysuiResTag("qqs_tile_layout"),
+ columns = GridCells.Fixed(columns)
+ ) {
items(
tiles.size,
key = { index -> sizedTiles[index].tile.spec.spec },
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt
index 7e6ccd6..9c0701e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt
@@ -22,7 +22,6 @@
import android.service.quicksettings.Tile.STATE_ACTIVE
import android.service.quicksettings.Tile.STATE_INACTIVE
import android.text.TextUtils
-import androidx.appcompat.content.res.AppCompatResources
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
@@ -593,15 +592,15 @@
}
@Composable
-private fun getTileIcon(icon: Supplier<QSTile.Icon>): Icon {
+private fun getTileIcon(icon: Supplier<QSTile.Icon?>): Icon {
val context = LocalContext.current
- return icon.get().let {
+ return icon.get()?.let {
if (it is QSTileImpl.ResourceIcon) {
Icon.Resource(it.resId, null)
} else {
Icon.Loaded(it.getDrawable(context), null)
}
- }
+ } ?: Icon.Resource(R.drawable.ic_error_outline, null)
}
@OptIn(ExperimentalAnimationGraphicsApi::class)
@@ -618,7 +617,7 @@
remember(icon, context) {
when (icon) {
is Icon.Loaded -> icon.drawable
- is Icon.Resource -> AppCompatResources.getDrawable(context, icon.res)
+ is Icon.Resource -> context.getDrawable(icon.res)
}
}
if (loadedDrawable !is Animatable) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileUiState.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileUiState.kt
index 4ec59c9..c83e3b2 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileUiState.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileUiState.kt
@@ -25,7 +25,7 @@
val label: String,
val secondaryLabel: String,
val state: Int,
- val icon: Supplier<QSTile.Icon>,
+ val icon: Supplier<QSTile.Icon?>,
)
fun QSTile.State.toUiState(): TileUiState {
@@ -33,6 +33,6 @@
label?.toString() ?: "",
secondaryLabel?.toString() ?: "",
state,
- icon?.let { Supplier { icon } } ?: iconSupplier,
+ icon?.let { Supplier { icon } } ?: iconSupplier ?: Supplier { null },
)
}
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 c4fbc37..94dd9bb 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,8 @@
import com.android.systemui.power.domain.interactor.PowerInteractor;
import com.android.systemui.qs.QSFragmentLegacy;
import com.android.systemui.qs.QSPanelController;
+import com.android.systemui.qs.composefragment.QSFragmentCompose;
+import com.android.systemui.qs.flags.QSComposeFragment;
import com.android.systemui.res.R;
import com.android.systemui.scene.domain.interactor.WindowRootViewVisibilityInteractor;
import com.android.systemui.scene.shared.flag.SceneContainerFlag;
@@ -1432,9 +1434,15 @@
}
protected QS createDefaultQSFragment() {
+ Class<? extends QS> klass;
+ if (QSComposeFragment.isEnabled()) {
+ klass = QSFragmentCompose.class;
+ } else {
+ klass = QSFragmentLegacy.class;
+ }
return mFragmentService
.getFragmentHostManager(getNotificationShadeWindowView())
- .create(QSFragmentLegacy.class);
+ .create(klass);
}
private void setUpPresenter() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSImplTest.java
index 206bbbf..4ce2d7c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSImplTest.java
@@ -51,6 +51,7 @@
import androidx.compose.ui.platform.ComposeView;
import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleOwner;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
@@ -611,7 +612,8 @@
when(mQSContainerImplController.getView()).thenReturn(mContainer);
when(mQSPanelController.getTileLayout()).thenReturn(mQQsTileLayout);
when(mQuickQSPanelController.getTileLayout()).thenReturn(mQsTileLayout);
- when(mFooterActionsViewModelFactory.create(any())).thenReturn(mFooterActionsViewModel);
+ when(mFooterActionsViewModelFactory.create(any(LifecycleOwner.class)))
+ .thenReturn(mFooterActionsViewModel);
}
private void setUpMedia() {
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeFgsManagerController.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeFgsManagerController.kt
index 9ff7dd5..ffe6918 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeFgsManagerController.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeFgsManagerController.kt
@@ -27,6 +27,9 @@
numRunningPackages: Int = 0,
) : FgsManagerController {
+ var initialized = false
+ private set
+
override var numRunningPackages = numRunningPackages
set(value) {
if (value != field) {
@@ -53,7 +56,9 @@
dialogDismissedListeners.forEach { it.onDialogDismissed() }
}
- override fun init() {}
+ override fun init() {
+ initialized = true
+ }
override fun showDialog(expandable: Expandable?) {}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelKosmos.kt
new file mode 100644
index 0000000..d37d8f3
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelKosmos.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.composefragment.viewmodel
+
+import android.content.res.mainResources
+import androidx.lifecycle.LifecycleCoroutineScope
+import com.android.systemui.common.ui.domain.interactor.configurationInteractor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.qs.footerActionsController
+import com.android.systemui.qs.footerActionsViewModelFactory
+import com.android.systemui.qs.ui.viewmodel.quickSettingsContainerViewModel
+import com.android.systemui.shade.largeScreenHeaderHelper
+import com.android.systemui.shade.transition.largeScreenShadeInterpolator
+import com.android.systemui.statusbar.disableflags.data.repository.disableFlagsRepository
+import com.android.systemui.statusbar.phone.keyguardBypassController
+import com.android.systemui.statusbar.sysuiStatusBarStateController
+
+val Kosmos.qsFragmentComposeViewModelFactory by
+ Kosmos.Fixture {
+ object : QSFragmentComposeViewModel.Factory {
+ override fun create(
+ lifecycleScope: LifecycleCoroutineScope
+ ): QSFragmentComposeViewModel {
+ return QSFragmentComposeViewModel(
+ quickSettingsContainerViewModel,
+ mainResources,
+ footerActionsViewModelFactory,
+ footerActionsController,
+ sysuiStatusBarStateController,
+ keyguardBypassController,
+ disableFlagsRepository,
+ largeScreenShadeInterpolator,
+ configurationInteractor,
+ largeScreenHeaderHelper,
+ lifecycleScope,
+ )
+ }
+ }
+ }