summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Fabian Kozynski <kozynski@google.com> 2024-01-23 16:20:32 +0000
committer Android (Google) Code Review <android-gerrit@google.com> 2024-01-23 16:20:32 +0000
commit567f24728992aa3d720b9c203ff7fa3dcffa1ff8 (patch)
tree8cd89c082ae63fa1881ff5387a01268ccb3f3e98
parent63320d3f2be6d070153415eb36a333cb65eb8b7f (diff)
parent6b66acfecf522945fa7d1187020dfdf970c35174 (diff)
Merge changes I1b3bafe4,I9957bd69,I8d764200 into main
* changes: Move scrollable to Compose Re-inflate on interesting config changes Use Compose FooterActions directly in Scene
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/applications/InterestingConfigChanges.java11
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt45
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt166
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsTheme.kt32
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt92
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt22
-rw-r--r--packages/SystemUI/src/com/android/systemui/common/ui/domain/interactor/ConfigurationInteractor.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java84
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/QSContainerImplController.java11
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/QSImpl.java97
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/QSPanel.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java4
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt48
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt15
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/QSImplTest.java52
15 files changed, 540 insertions, 144 deletions
diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/InterestingConfigChanges.java b/packages/SettingsLib/src/com/android/settingslib/applications/InterestingConfigChanges.java
index 5d520ce5d81f..7e2d0af5c075 100644
--- a/packages/SettingsLib/src/com/android/settingslib/applications/InterestingConfigChanges.java
+++ b/packages/SettingsLib/src/com/android/settingslib/applications/InterestingConfigChanges.java
@@ -21,6 +21,8 @@ import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.content.res.Resources;
+import androidx.annotation.NonNull;
+
/**
* A class for applying config changes and determing if doing so resulting in any "interesting"
* changes.
@@ -48,8 +50,15 @@ public class InterestingConfigChanges {
*/
@SuppressLint("NewApi")
public boolean applyNewConfig(Resources res) {
+ return applyNewConfig(res.getConfiguration());
+ }
+
+ /**
+ * Applies the given config change and returns whether an "interesting" change happened.
+ */
+ public boolean applyNewConfig(@NonNull Configuration configuration) {
int configChanges = mLastConfiguration.updateFrom(
- Configuration.generateDelta(mLastConfiguration, res.getConfiguration()));
+ Configuration.generateDelta(mLastConfiguration, configuration));
return (configChanges & (mFlags)) != 0;
}
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt
index 9778e53d8f69..c027c499c0b7 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt
@@ -16,17 +16,16 @@
package com.android.systemui.qs.ui.composable
-import android.view.ContextThemeWrapper
import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.defaultMinSize
-import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.wrapContentHeight
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.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
@@ -53,14 +52,6 @@ object QuickSettings {
}
}
-@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() }
-}
-
private fun SceneScope.stateForQuickSettingsContent(): QSSceneAdapter.State {
return when (val transitionState = layoutState.transitionState) {
is TransitionState.Idle -> {
@@ -115,6 +106,7 @@ private fun QuickSettingsContent(
modifier: Modifier = Modifier,
) {
val qsView by qsSceneAdapter.qsView.collectAsState(null)
+ val isCustomizing by qsSceneAdapter.isCustomizing.collectAsState()
QuickSettingsTheme {
val context = LocalContext.current
@@ -124,14 +116,27 @@ private fun QuickSettingsContent(
}
}
qsView?.let { view ->
- AndroidView(
- modifier = modifier.fillMaxSize().background(colorAttr(R.attr.underSurface)),
- factory = { _ ->
- qsSceneAdapter.setState(state)
- view
- },
- update = { qsSceneAdapter.setState(state) }
- )
+ Box(
+ modifier =
+ modifier
+ .fillMaxWidth()
+ .then(
+ if (isCustomizing) {
+ Modifier.fillMaxHeight()
+ } else {
+ Modifier.wrapContentHeight()
+ }
+ )
+ ) {
+ AndroidView(
+ modifier = Modifier.fillMaxWidth().background(colorAttr(R.attr.underSurface)),
+ factory = { _ ->
+ qsSceneAdapter.setState(state)
+ view
+ },
+ update = { qsSceneAdapter.setState(state) }
+ )
+ }
}
}
}
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 d8c7290b76b8..bbfe0fda049a 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
@@ -24,31 +24,44 @@ import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.background
+import androidx.compose.foundation.clipScrollableContainer
+import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Box
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.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.unit.dp
import com.android.compose.animation.scene.SceneScope
+import com.android.compose.animation.scene.TransitionState
import com.android.compose.windowsizeclass.LocalWindowSizeClass
import com.android.systemui.battery.BatteryMeterViewController
+import com.android.systemui.compose.modifiers.sysuiResTag
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.notifications.ui.composable.HeadsUpNotificationSpace
+import com.android.systemui.qs.footer.ui.compose.FooterActions
import com.android.systemui.qs.ui.viewmodel.QuickSettingsSceneViewModel
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.ui.composable.ComposableScene
+import com.android.systemui.scene.ui.composable.toTransitionSceneKey
import com.android.systemui.shade.ui.composable.CollapsedShadeHeader
import com.android.systemui.shade.ui.composable.ExpandedShadeHeader
import com.android.systemui.shade.ui.composable.Shade
@@ -105,57 +118,120 @@ private fun SceneScope.QuickSettingsScene(
) {
// TODO(b/280887232): implement the real UI.
Box(modifier = modifier.fillMaxSize()) {
- Box(modifier = Modifier.fillMaxSize()) {
- val isCustomizing by viewModel.qsSceneAdapter.isCustomizing.collectAsState()
- val collapsedHeaderHeight =
- with(LocalDensity.current) { ShadeHeader.Dimensions.CollapsedHeight.roundToPx() }
- Spacer(
- modifier =
- Modifier.element(Shade.Elements.ScrimBackground)
- .fillMaxSize()
- .background(MaterialTheme.colorScheme.scrim, shape = Shade.Shapes.Scrim)
- )
- Column(
- horizontalAlignment = Alignment.CenterHorizontally,
- modifier =
- Modifier.fillMaxSize().padding(start = 16.dp, end = 16.dp, bottom = 48.dp)
- ) {
- when (LocalWindowSizeClass.current.widthSizeClass) {
- WindowWidthSizeClass.Compact ->
- 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(
+ val isCustomizing by viewModel.qsSceneAdapter.isCustomizing.collectAsState()
+ val collapsedHeaderHeight =
+ with(LocalDensity.current) { ShadeHeader.Dimensions.CollapsedHeight.roundToPx() }
+ val lifecycleOwner = LocalLifecycleOwner.current
+ val footerActionsViewModel =
+ remember(lifecycleOwner, viewModel) {
+ viewModel.getFooterActionsViewModel(lifecycleOwner)
+ }
+ val scrollState = rememberScrollState()
+ // When animating into the scene, we don't want it to be able to scroll, as it could mess
+ // up with the expansion animation.
+ val isScrollable =
+ when (val state = layoutState.transitionState) {
+ is TransitionState.Idle -> true
+ is TransitionState.Transition -> {
+ state.fromScene == SceneKey.QuickSettings.toTransitionSceneKey()
+ }
+ }
+
+ LaunchedEffect(isCustomizing, scrollState) {
+ if (isCustomizing) {
+ scrollState.scrollTo(0)
+ }
+ }
+
+ // This is the background for the whole scene, as the elements don't necessarily provide
+ // a background that extends to the edges.
+ Spacer(
+ modifier =
+ Modifier.element(Shade.Elements.ScrimBackground)
+ .fillMaxSize()
+ .background(MaterialTheme.colorScheme.scrim, shape = Shade.Shapes.Scrim)
+ )
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier =
+ Modifier.fillMaxSize()
+ // bottom should be tied to insets
+ .padding(bottom = 16.dp)
+ ) {
+ Box(modifier = Modifier.fillMaxSize().weight(1f)) {
+ val shadeHeaderAndQuickSettingsModifier =
+ if (isCustomizing) {
+ Modifier.fillMaxHeight().align(Alignment.TopCenter)
+ } else {
+ Modifier.verticalNestedScrollToScene()
+ .verticalScroll(
+ scrollState,
+ enabled = isScrollable,
+ )
+ .clipScrollableContainer(Orientation.Horizontal)
+ .fillMaxWidth()
+ .wrapContentHeight(unbounded = true)
+ .align(Alignment.TopCenter)
+ }
+
+ Column(
+ modifier = shadeHeaderAndQuickSettingsModifier,
+ ) {
+ when (LocalWindowSizeClass.current.widthSizeClass) {
+ WindowWidthSizeClass.Compact ->
+ AnimatedVisibility(
+ visible = !isCustomizing,
+ enter =
+ expandVertically(
+ animationSpec = tween(100),
+ initialHeight = { collapsedHeaderHeight },
+ ) + fadeIn(tween(100)),
+ exit =
+ shrinkVertically(
+ animationSpec = tween(100),
+ targetHeight = { collapsedHeaderHeight },
+ shrinkTowards = Alignment.Top,
+ ) + fadeOut(tween(100)),
+ ) {
+ ExpandedShadeHeader(
+ viewModel = viewModel.shadeHeaderViewModel,
+ createTintedIconManager = createTintedIconManager,
+ createBatteryMeterViewController =
+ createBatteryMeterViewController,
+ statusBarIconController = statusBarIconController,
+ modifier = Modifier.padding(horizontal = 16.dp),
+ )
+ }
+ else ->
+ CollapsedShadeHeader(
viewModel = viewModel.shadeHeaderViewModel,
createTintedIconManager = createTintedIconManager,
createBatteryMeterViewController = createBatteryMeterViewController,
statusBarIconController = statusBarIconController,
+ modifier = Modifier.padding(horizontal = 16.dp),
)
- }
- else ->
- CollapsedShadeHeader(
- viewModel = viewModel.shadeHeaderViewModel,
- createTintedIconManager = createTintedIconManager,
- createBatteryMeterViewController = createBatteryMeterViewController,
- statusBarIconController = statusBarIconController,
- )
+ }
+ Spacer(modifier = Modifier.height(16.dp))
+ // This view has its own horizontal padding
+ QuickSettings(
+ modifier = Modifier.sysuiResTag("expanded_qs_scroll_view"),
+ viewModel.qsSceneAdapter,
+ )
+ }
+ }
+ AnimatedVisibility(
+ visible = !isCustomizing,
+ modifier = Modifier.align(Alignment.CenterHorizontally).fillMaxWidth()
+ ) {
+ QuickSettingsTheme {
+ // This view has its own horizontal padding
+ // TODO(b/321716470) This should use a lifecycle tied to the scene.
+ FooterActions(
+ viewModel = footerActionsViewModel,
+ qsVisibilityLifecycleOwner = lifecycleOwner,
+ modifier = Modifier.element(QuickSettings.Elements.FooterActions)
+ )
}
- Spacer(modifier = Modifier.height(16.dp))
- QuickSettings(
- modifier = Modifier.fillMaxHeight(),
- viewModel.qsSceneAdapter,
- )
}
}
HeadsUpNotificationSpace(
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsTheme.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsTheme.kt
new file mode 100644
index 000000000000..87b6f95b0ae6
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsTheme.kt
@@ -0,0 +1,32 @@
+/*
+ * 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 androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.remember
+import androidx.compose.ui.platform.LocalContext
+import com.android.systemui.res.R
+
+@Composable
+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() }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt
index d9b1ea1aedcc..cae20d006dec 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt
@@ -16,12 +16,16 @@
package com.android.systemui.qs.ui.adapter
+import android.content.res.Configuration
import android.os.Bundle
+import android.view.Surface
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.common.ui.data.repository.FakeConfigurationRepository
+import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.qs.QSImpl
import com.android.systemui.qs.dagger.QSComponent
@@ -34,6 +38,7 @@ 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 java.util.Locale
import javax.inject.Provider
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
@@ -81,11 +86,17 @@ class QSSceneAdapterImplTest : SysuiTestCase() {
.also { components.add(it) }
}
}
+ private val configuration = Configuration(context.resources.configuration)
+
+ private val fakeConfigurationRepository =
+ FakeConfigurationRepository().apply { onConfigurationChange(configuration) }
+ private val configurationInteractor = ConfigurationInteractor(fakeConfigurationRepository)
private val mockAsyncLayoutInflater =
mock<AsyncLayoutInflater>() {
whenever(inflate(anyInt(), nullable(), any())).then { invocation ->
val mockView = mock<View>()
+ whenever(mockView.context).thenReturn(context)
invocation
.getArgument<AsyncLayoutInflater.OnInflateFinishedListener>(2)
.onInflateFinished(
@@ -102,6 +113,7 @@ class QSSceneAdapterImplTest : SysuiTestCase() {
qsImplProvider,
testDispatcher,
testScope.backgroundScope,
+ configurationInteractor,
{ mockAsyncLayoutInflater },
)
@@ -297,6 +309,9 @@ class QSSceneAdapterImplTest : SysuiTestCase() {
@Test
fun reinflation_previousStateDestroyed() =
testScope.runTest {
+ // Run all flows... In particular, initial configuration propagation that could cause
+ // QSImpl to re-inflate.
+ runCurrent()
val qsImpl by collectLastValue(underTest.qsImpl)
underTest.inflate(context)
@@ -322,4 +337,81 @@ class QSSceneAdapterImplTest : SysuiTestCase() {
bundleArgCaptor.value,
)
}
+
+ @Test
+ fun changeInLocale_reinflation() =
+ testScope.runTest {
+ val qsImpl by collectLastValue(underTest.qsImpl)
+
+ underTest.inflate(context)
+ runCurrent()
+
+ val oldQsImpl = qsImpl!!
+
+ val newLocale =
+ if (configuration.locales[0] == Locale("en-US")) {
+ Locale("es-UY")
+ } else {
+ Locale("en-US")
+ }
+ configuration.setLocale(newLocale)
+ fakeConfigurationRepository.onConfigurationChange(configuration)
+ runCurrent()
+
+ assertThat(oldQsImpl).isNotSameInstanceAs(qsImpl!!)
+ }
+
+ @Test
+ fun changeInFontSize_reinflation() =
+ testScope.runTest {
+ val qsImpl by collectLastValue(underTest.qsImpl)
+
+ underTest.inflate(context)
+ runCurrent()
+
+ val oldQsImpl = qsImpl!!
+
+ configuration.fontScale *= 2
+ fakeConfigurationRepository.onConfigurationChange(configuration)
+ runCurrent()
+
+ assertThat(oldQsImpl).isNotSameInstanceAs(qsImpl!!)
+ }
+
+ @Test
+ fun changeInAssetPath_reinflation() =
+ testScope.runTest {
+ val qsImpl by collectLastValue(underTest.qsImpl)
+
+ underTest.inflate(context)
+ runCurrent()
+
+ val oldQsImpl = qsImpl!!
+
+ configuration.assetsSeq += 1
+ fakeConfigurationRepository.onConfigurationChange(configuration)
+ runCurrent()
+
+ assertThat(oldQsImpl).isNotSameInstanceAs(qsImpl!!)
+ }
+
+ @Test
+ fun otherChangesInConfiguration_noReinflation_configurationChangeDispatched() =
+ testScope.runTest {
+ val qsImpl by collectLastValue(underTest.qsImpl)
+
+ underTest.inflate(context)
+ runCurrent()
+
+ val oldQsImpl = qsImpl!!
+ configuration.densityDpi *= 2
+ configuration.windowConfiguration.maxBounds.scale(2f)
+ configuration.windowConfiguration.rotation = Surface.ROTATION_270
+ fakeConfigurationRepository.onConfigurationChange(configuration)
+ runCurrent()
+
+ assertThat(oldQsImpl).isSameInstanceAs(qsImpl!!)
+ verify(qsImpl!!).onConfigurationChanged(configuration)
+ verify(qsImpl!!.view).dispatchConfigurationChanged(configuration)
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt
index d7a794149869..42200a3d33ec 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt
@@ -23,6 +23,8 @@ import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.flags.FakeFeatureFlagsClassic
import com.android.systemui.flags.Flags
import com.android.systemui.kosmos.testScope
+import com.android.systemui.qs.FooterActionsController
+import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
import com.android.systemui.qs.ui.adapter.FakeQSSceneAdapter
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.shared.model.Direction
@@ -39,12 +41,16 @@ import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsVi
import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository
import com.android.systemui.testKosmos
+import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
@SmallTest
@RunWith(AndroidJUnit4::class)
@@ -56,6 +62,12 @@ class QuickSettingsSceneViewModelTest : SysuiTestCase() {
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 val footerActionsViewModel = mock<FooterActionsViewModel>()
+ private val footerActionsViewModelFactory =
+ mock<FooterActionsViewModel.Factory> {
+ whenever(create(any())).thenReturn(footerActionsViewModel)
+ }
+ private val footerActionsController = mock<FooterActionsController>()
private var mobileIconsViewModel: MobileIconsViewModel =
MobileIconsViewModel(
@@ -94,6 +106,8 @@ class QuickSettingsSceneViewModelTest : SysuiTestCase() {
shadeHeaderViewModel = shadeHeaderViewModel,
qsSceneAdapter = qsFlexiglassAdapter,
notifications = kosmos.notificationsPlaceholderViewModel,
+ footerActionsViewModelFactory,
+ footerActionsController,
)
}
@@ -125,4 +139,12 @@ class QuickSettingsSceneViewModelTest : SysuiTestCase() {
)
)
}
+
+ @Test
+ fun gettingViewModelInitializesControllerOnlyOnce() {
+ underTest.getFooterActionsViewModel(mock())
+ underTest.getFooterActionsViewModel(mock())
+
+ verify(footerActionsController, times(1)).init()
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/domain/interactor/ConfigurationInteractor.kt b/packages/SystemUI/src/com/android/systemui/common/ui/domain/interactor/ConfigurationInteractor.kt
index 13539850a598..5f6ff82c6038 100644
--- a/packages/SystemUI/src/com/android/systemui/common/ui/domain/interactor/ConfigurationInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/ui/domain/interactor/ConfigurationInteractor.kt
@@ -72,6 +72,9 @@ class ConfigurationInteractor @Inject constructor(private val repository: Config
val onAnyConfigurationChange: Flow<Unit> =
repository.onAnyConfigurationChange.onStart { emit(Unit) }
+ /** Emits the new configuration on any configuration change */
+ val configurationValues: Flow<Configuration> = repository.configurationValues
+
/** Emits the current resolution scaling factor */
val scaleForResolution: Flow<Float> = repository.scaleForResolution
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
index a3b92541d593..a2dfc0159c6e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
@@ -27,8 +27,11 @@ import android.graphics.PointF;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
+import android.view.ViewGroup;
import android.widget.FrameLayout;
+import androidx.annotation.Nullable;
+
import com.android.systemui.Dumpable;
import com.android.systemui.qs.customize.QSCustomizer;
import com.android.systemui.res.R;
@@ -53,6 +56,7 @@ public class QSContainerImpl extends FrameLayout implements Dumpable {
private QuickStatusBarHeader mHeader;
private float mQsExpansion;
private QSCustomizer mQSCustomizer;
+ private QSPanel mQSPanel;
private NonInterceptingScrollView mQSPanelContainer;
private int mHorizontalMargins;
@@ -72,6 +76,7 @@ public class QSContainerImpl extends FrameLayout implements Dumpable {
protected void onFinishInflate() {
super.onFinishInflate();
mQSPanelContainer = findViewById(R.id.expanded_qs_scroll_view);
+ mQSPanel = findViewById(R.id.quick_settings_panel);
mHeader = findViewById(R.id.header);
mQSCustomizer = findViewById(R.id.qs_customize);
setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
@@ -79,6 +84,13 @@ public class QSContainerImpl extends FrameLayout implements Dumpable {
void setSceneContainerEnabled(boolean enabled) {
mSceneContainerEnabled = enabled;
+ if (enabled) {
+ mQSPanelContainer.removeAllViews();
+ removeView(mQSPanelContainer);
+ LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT);
+ addView(mQSPanel, 0, lp);
+ }
}
@Override
@@ -97,20 +109,26 @@ public class QSContainerImpl extends FrameLayout implements Dumpable {
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// QSPanel will show as many rows as it can (up to TileLayout.MAX_ROWS) such that the
// bottom and footer are inside the screen.
- MarginLayoutParams layoutParams = (MarginLayoutParams) mQSPanelContainer.getLayoutParams();
-
int availableHeight = View.MeasureSpec.getSize(heightMeasureSpec);
- int maxQs = availableHeight - layoutParams.topMargin - layoutParams.bottomMargin
- - getPaddingBottom();
- int padding = mPaddingLeft + mPaddingRight + layoutParams.leftMargin
- + layoutParams.rightMargin;
- final int qsPanelWidthSpec = getChildMeasureSpec(widthMeasureSpec, padding,
- layoutParams.width);
- mQSPanelContainer.measure(qsPanelWidthSpec,
- MeasureSpec.makeMeasureSpec(maxQs, MeasureSpec.AT_MOST));
- int width = mQSPanelContainer.getMeasuredWidth() + padding;
- super.onMeasure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
- MeasureSpec.makeMeasureSpec(availableHeight, MeasureSpec.EXACTLY));
+
+ if (!mSceneContainerEnabled) {
+ MarginLayoutParams layoutParams =
+ (MarginLayoutParams) mQSPanelContainer.getLayoutParams();
+ int maxQs = availableHeight - layoutParams.topMargin - layoutParams.bottomMargin
+ - getPaddingBottom();
+ int padding = mPaddingLeft + mPaddingRight + layoutParams.leftMargin
+ + layoutParams.rightMargin;
+ final int qsPanelWidthSpec = getChildMeasureSpec(widthMeasureSpec, padding,
+ layoutParams.width);
+ mQSPanelContainer.measure(qsPanelWidthSpec,
+ MeasureSpec.makeMeasureSpec(maxQs, MeasureSpec.AT_MOST));
+ int width = mQSPanelContainer.getMeasuredWidth() + padding;
+ super.onMeasure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(availableHeight, MeasureSpec.EXACTLY));
+ } else {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+
// QSCustomizer will always be the height of the screen, but do this after
// other measuring to avoid changing the height of the QS.
mQSCustomizer.measure(widthMeasureSpec,
@@ -130,12 +148,15 @@ public class QSContainerImpl extends FrameLayout implements Dumpable {
@Override
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
- // Do not measure QSPanel again when doing super.onMeasure.
- // This prevents the pages in PagedTileLayout to be remeasured with a different (incorrect)
- // size to the one used for determining the number of rows and then the number of pages.
- if (child != mQSPanelContainer) {
- super.measureChildWithMargins(child, parentWidthMeasureSpec, widthUsed,
- parentHeightMeasureSpec, heightUsed);
+ if (!mSceneContainerEnabled) {
+ // Do not measure QSPanel again when doing super.onMeasure.
+ // This prevents the pages in PagedTileLayout to be remeasured with a different
+ // (incorrect) size to the one used for determining the number of rows and then the
+ // number of pages.
+ if (child != mQSPanelContainer) {
+ super.measureChildWithMargins(child, parentWidthMeasureSpec, widthUsed,
+ parentHeightMeasureSpec, heightUsed);
+ }
}
}
@@ -151,6 +172,7 @@ public class QSContainerImpl extends FrameLayout implements Dumpable {
updateClippingPath();
}
+ @Nullable
public NonInterceptingScrollView getQSPanelContainer() {
return mQSPanelContainer;
}
@@ -172,11 +194,19 @@ public class QSContainerImpl extends FrameLayout implements Dumpable {
.getDimensionPixelSize(
R.dimen.large_screen_shade_header_height);
}
- mQSPanelContainer.setPaddingRelative(
- mQSPanelContainer.getPaddingStart(),
- mSceneContainerEnabled ? 0 : topPadding,
- mQSPanelContainer.getPaddingEnd(),
- mQSPanelContainer.getPaddingBottom());
+ if (mQSPanelContainer != null) {
+ mQSPanelContainer.setPaddingRelative(
+ mQSPanelContainer.getPaddingStart(),
+ mSceneContainerEnabled ? 0 : topPadding,
+ mQSPanelContainer.getPaddingEnd(),
+ mQSPanelContainer.getPaddingBottom());
+ } else {
+ mQSPanel.setPaddingRelative(
+ mQSPanel.getPaddingStart(),
+ mSceneContainerEnabled ? 0 : topPadding,
+ mQSPanel.getPaddingEnd(),
+ mQSPanel.getPaddingBottom());
+ }
int horizontalMargins = getResources().getDimensionPixelSize(R.dimen.qs_horizontal_margin);
int horizontalPadding = getResources().getDimensionPixelSize(
@@ -220,7 +250,9 @@ public class QSContainerImpl extends FrameLayout implements Dumpable {
public void setExpansion(float expansion) {
mQsExpansion = expansion;
- mQSPanelContainer.setScrollingEnabled(expansion > 0f);
+ if (mQSPanelContainer != null) {
+ mQSPanelContainer.setScrollingEnabled(expansion > 0f);
+ }
updateExpansion();
}
@@ -239,7 +271,7 @@ public class QSContainerImpl extends FrameLayout implements Dumpable {
lp.rightMargin = mHorizontalMargins;
lp.leftMargin = mHorizontalMargins;
}
- if (view == mQSPanelContainer) {
+ if (view == mQSPanelContainer || view == mQSPanel) {
// QS panel lays out some of its content full width
qsPanelController.setContentMargins(mContentHorizontalPadding,
mContentHorizontalPadding);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImplController.java b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImplController.java
index 7b001c7b72f7..ffbc56098e26 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImplController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImplController.java
@@ -81,6 +81,9 @@ public class QSContainerImplController extends ViewController<QSContainerImpl> {
public void onInit() {
mQuickStatusBarHeaderController.init();
mView.setSceneContainerEnabled(mSceneContainerEnabled);
+ if (mSceneContainerEnabled && mQsPanelController != null) {
+ mQSPanelContainer.setOnTouchListener(null);
+ }
}
public void setListening(boolean listening) {
@@ -91,13 +94,17 @@ public class QSContainerImplController extends ViewController<QSContainerImpl> {
protected void onViewAttached() {
mView.updateResources(mQsPanelController, mQuickStatusBarHeaderController);
mConfigurationController.addCallback(mConfigurationListener);
- mQSPanelContainer.setOnTouchListener(mContainerTouchHandler);
+ if (!mSceneContainerEnabled && mQSPanelContainer != null) {
+ mQSPanelContainer.setOnTouchListener(mContainerTouchHandler);
+ }
}
@Override
protected void onViewDetached() {
mConfigurationController.removeCallback(mConfigurationListener);
- mQSPanelContainer.setOnTouchListener(null);
+ if (mQSPanelContainer != null) {
+ mQSPanelContainer.setOnTouchListener(null);
+ }
}
public QSContainerImpl getView() {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSImpl.java b/packages/SystemUI/src/com/android/systemui/qs/QSImpl.java
index 7f91fd2ebb80..290821e4ab13 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSImpl.java
@@ -61,6 +61,7 @@ import com.android.systemui.qs.footer.ui.binder.FooterActionsViewBinder;
import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel;
import com.android.systemui.qs.logging.QSLogger;
import com.android.systemui.res.R;
+import com.android.systemui.scene.shared.flag.SceneContainerFlags;
import com.android.systemui.shade.transition.LargeScreenShadeInterpolator;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.StatusBarState;
@@ -171,8 +172,11 @@ public class QSImpl implements QS, CommandQueue.Callbacks, StatusBarStateControl
private CommandQueue mCommandQueue;
private View mRootView;
+ @Nullable
private View mFooterActionsView;
+ private final SceneContainerFlags mSceneContainerFlags;
+
@Inject
public QSImpl(RemoteInputQuickSettingsDisabler remoteInputQsDisabler,
SysuiStatusBarStateController statusBarStateController, CommandQueue commandQueue,
@@ -185,7 +189,8 @@ public class QSImpl implements QS, CommandQueue.Callbacks, StatusBarStateControl
FooterActionsViewModel.Factory footerActionsViewModelFactory,
FooterActionsViewBinder footerActionsViewBinder,
LargeScreenShadeInterpolator largeScreenShadeInterpolator,
- FeatureFlags featureFlags) {
+ FeatureFlags featureFlags,
+ SceneContainerFlags sceneContainerFlags) {
mRemoteInputQuickSettingsDisabler = remoteInputQsDisabler;
mQsMediaHost = qsMediaHost;
mQqsMediaHost = qqsMediaHost;
@@ -201,6 +206,7 @@ public class QSImpl implements QS, CommandQueue.Callbacks, StatusBarStateControl
mFooterActionsViewModelFactory = footerActionsViewModelFactory;
mFooterActionsViewBinder = footerActionsViewBinder;
mListeningAndVisibilityLifecycleOwner = new ListeningAndVisibilityLifecycleOwner();
+ mSceneContainerFlags = sceneContainerFlags;
}
/**
@@ -216,10 +222,17 @@ public class QSImpl implements QS, CommandQueue.Callbacks, StatusBarStateControl
mQSPanelController.init();
mQuickQSPanelController.init();
- mQSFooterActionsViewModel = mFooterActionsViewModelFactory
- .create(mListeningAndVisibilityLifecycleOwner);
- bindFooterActionsView(mRootView);
- mFooterActionsController.init();
+ if (!mSceneContainerFlags.isEnabled()) {
+ mQSFooterActionsViewModel = mFooterActionsViewModelFactory
+ .create(mListeningAndVisibilityLifecycleOwner);
+ bindFooterActionsView(mRootView);
+ mFooterActionsController.init();
+ } else {
+ View footerView = mRootView.findViewById(R.id.qs_footer_actions);
+ if (footerView != null) {
+ ((ViewGroup) footerView.getParent()).removeView(footerView);
+ }
+ }
mQSPanelScrollView = mRootView.findViewById(R.id.expanded_qs_scroll_view);
mQSPanelScrollView.addOnLayoutChangeListener(
@@ -234,6 +247,7 @@ public class QSImpl implements QS, CommandQueue.Callbacks, StatusBarStateControl
mScrollListener.onQsPanelScrollChanged(scrollY);
}
});
+ mQSPanelScrollView.setScrollingEnabled(!mSceneContainerFlags.isEnabled());
mHeader = mRootView.findViewById(R.id.header);
mFooter = qsComponent.getQSFooter();
@@ -481,7 +495,9 @@ public class QSImpl implements QS, CommandQueue.Callbacks, StatusBarStateControl
boolean footerVisible = qsPanelVisible && (mQsExpanded || !keyguardShowing
|| mHeaderAnimating || mShowCollapsedOnKeyguard);
mFooter.setVisibility(footerVisible ? View.VISIBLE : View.INVISIBLE);
- mFooterActionsView.setVisibility(footerVisible ? View.VISIBLE : View.INVISIBLE);
+ if (mFooterActionsView != null) {
+ mFooterActionsView.setVisibility(footerVisible ? View.VISIBLE : View.INVISIBLE);
+ }
mFooter.setExpanded((keyguardShowing && !mHeaderAnimating && !mShowCollapsedOnKeyguard)
|| (mQsExpanded && !mStackScrollerOverscrolling));
mQSPanelController.setVisibility(qsPanelVisible ? View.VISIBLE : View.INVISIBLE);
@@ -622,8 +638,13 @@ public class QSImpl implements QS, CommandQueue.Callbacks, StatusBarStateControl
@Override
public int getHeightDiff() {
- return mQSPanelScrollView.getBottom() - mHeader.getBottom()
- + mHeader.getPaddingBottom();
+ if (mSceneContainerFlags.isEnabled()) {
+ return mQSPanelController.getViewBottom() - mHeader.getBottom()
+ + mHeader.getPaddingBottom();
+ } else {
+ return mQSPanelScrollView.getBottom() - mHeader.getBottom()
+ + mHeader.getPaddingBottom();
+ }
}
@Override
@@ -678,25 +699,29 @@ public class QSImpl implements QS, CommandQueue.Callbacks, StatusBarStateControl
mFooter.setExpansion(onKeyguardAndExpanded ? 1 : expansion);
float footerActionsExpansion =
onKeyguardAndExpanded ? 1 : mInSplitShade ? alphaProgress : expansion;
- mQSFooterActionsViewModel.onQuickSettingsExpansionChanged(footerActionsExpansion,
- mInSplitShade);
+ if (mQSFooterActionsViewModel != null) {
+ mQSFooterActionsViewModel.onQuickSettingsExpansionChanged(footerActionsExpansion,
+ mInSplitShade);
+ }
mQSPanelController.setRevealExpansion(expansion);
mQSPanelController.getTileLayout().setExpansion(expansion, proposedTranslation);
mQuickQSPanelController.getTileLayout().setExpansion(expansion, proposedTranslation);
- float qsScrollViewTranslation =
- onKeyguard && !mShowCollapsedOnKeyguard ? panelTranslationY : 0;
- mQSPanelScrollView.setTranslationY(qsScrollViewTranslation);
+ if (!mSceneContainerFlags.isEnabled()) {
+ float qsScrollViewTranslation =
+ onKeyguard && !mShowCollapsedOnKeyguard ? panelTranslationY : 0;
+ mQSPanelScrollView.setTranslationY(qsScrollViewTranslation);
- if (fullyCollapsed) {
- mQSPanelScrollView.setScrollY(0);
- }
+ if (fullyCollapsed) {
+ mQSPanelScrollView.setScrollY(0);
+ }
- if (!fullyExpanded) {
- // Set bounds on the QS panel so it doesn't run over the header when animating.
- mQsBounds.top = (int) -mQSPanelScrollView.getTranslationY();
- mQsBounds.right = mQSPanelScrollView.getWidth();
- mQsBounds.bottom = mQSPanelScrollView.getHeight();
+ if (!fullyExpanded) {
+ // Set bounds on the QS panel so it doesn't run over the header when animating.
+ mQsBounds.top = (int) -mQSPanelScrollView.getTranslationY();
+ mQsBounds.right = mQSPanelScrollView.getWidth();
+ mQsBounds.bottom = mQSPanelScrollView.getHeight();
+ }
}
updateQsBounds();
@@ -786,15 +811,17 @@ public class QSImpl implements QS, CommandQueue.Callbacks, StatusBarStateControl
mQsBounds.set(-sideMargin, 0, mQSPanelScrollView.getWidth() + sideMargin,
mQSPanelScrollView.getHeight());
}
- mQSPanelScrollView.setClipBounds(mQsBounds);
-
- mQSPanelScrollView.getLocationOnScreen(mLocationTemp);
- int left = mLocationTemp[0];
- int top = mLocationTemp[1];
- mQsMediaHost.getCurrentClipping().set(left, top,
- left + getView().getMeasuredWidth(),
- top + mQSPanelScrollView.getMeasuredHeight()
- - mQSPanelController.getPaddingBottom());
+ if (!mSceneContainerFlags.isEnabled()) {
+ mQSPanelScrollView.setClipBounds(mQsBounds);
+
+ mQSPanelScrollView.getLocationOnScreen(mLocationTemp);
+ int left = mLocationTemp[0];
+ int top = mLocationTemp[1];
+ mQsMediaHost.getCurrentClipping().set(left, top,
+ left + getView().getMeasuredWidth(),
+ top + mQSPanelScrollView.getMeasuredHeight()
+ - mQSPanelController.getPaddingBottom());
+ }
}
private void updateMediaPositions() {
@@ -867,9 +894,15 @@ public class QSImpl implements QS, CommandQueue.Callbacks, StatusBarStateControl
// The customize state changed, so our height changed.
mContainer.updateExpansion();
boolean customizing = isCustomizing();
- mQSPanelScrollView.setVisibility(!customizing ? View.VISIBLE : View.INVISIBLE);
+ if (mSceneContainerFlags.isEnabled()) {
+ mQSPanelController.setVisibility(!customizing ? View.VISIBLE : View.INVISIBLE);
+ } else {
+ mQSPanelScrollView.setVisibility(!customizing ? View.VISIBLE : View.INVISIBLE);
+ }
mFooter.setVisibility(!customizing ? View.VISIBLE : View.INVISIBLE);
- mFooterActionsView.setVisibility(!customizing ? View.VISIBLE : View.INVISIBLE);
+ if (mFooterActionsView != null) {
+ mFooterActionsView.setVisibility(!customizing ? View.VISIBLE : View.INVISIBLE);
+ }
mHeader.setVisibility(!customizing ? View.VISIBLE : View.INVISIBLE);
// Let the panel know the position changed and it needs to update where notifications
// and whatnot are.
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
index 51b94dd983f3..7a7ee59fa63f 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
@@ -387,7 +387,7 @@ public class QSPanel extends LinearLayout implements Tunable {
setPaddingRelative(getPaddingStart(),
mSceneContainerEnabled ? 0 : paddingTop,
getPaddingEnd(),
- paddingBottom);
+ mSceneContainerEnabled ? 0 : paddingBottom);
}
void addOnConfigurationChangedListener(OnConfigurationChangedListener listener) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java
index ef58a608aa1f..c3f5086b0096 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java
@@ -278,5 +278,9 @@ public class QSPanelController extends QSPanelControllerBase<QSPanel> {
public int getPaddingBottom() {
return mView.getPaddingBottom();
}
+
+ int getViewBottom() {
+ return mView.getBottom();
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt
index ce840eec29d9..0d4339680dac 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt
@@ -17,10 +17,13 @@
package com.android.systemui.qs.ui.adapter
import android.content.Context
+import android.content.pm.ActivityInfo
import android.os.Bundle
import android.view.View
import androidx.annotation.VisibleForTesting
import androidx.asynclayoutinflater.view.AsyncLayoutInflater
+import com.android.settingslib.applications.InterestingConfigChanges
+import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Main
@@ -58,7 +61,7 @@ interface QSSceneAdapter {
/**
* Inflate an instance of [QSImpl] for this context. Once inflated, it will be available in
- * [qsView]
+ * [qsView]. Re-inflations due to configuration changes will use the last used [context].
*/
suspend fun inflate(context: Context)
@@ -90,6 +93,7 @@ constructor(
private val qsImplProvider: Provider<QSImpl>,
@Main private val mainDispatcher: CoroutineDispatcher,
@Application applicationScope: CoroutineScope,
+ private val configurationInteractor: ConfigurationInteractor,
private val asyncLayoutInflaterFactory: (Context) -> AsyncLayoutInflater,
) : QSContainerController, QSSceneAdapter {
@@ -99,7 +103,15 @@ constructor(
qsImplProvider: Provider<QSImpl>,
@Main dispatcher: CoroutineDispatcher,
@Application scope: CoroutineScope,
- ) : this(qsSceneComponentFactory, qsImplProvider, dispatcher, scope, ::AsyncLayoutInflater)
+ configurationInteractor: ConfigurationInteractor,
+ ) : this(
+ qsSceneComponentFactory,
+ qsImplProvider,
+ dispatcher,
+ scope,
+ configurationInteractor,
+ ::AsyncLayoutInflater,
+ )
private val state = MutableStateFlow<QSSceneAdapter.State>(QSSceneAdapter.State.CLOSED)
private val _isCustomizing: MutableStateFlow<Boolean> = MutableStateFlow(false)
@@ -109,14 +121,36 @@ constructor(
val qsImpl = _qsImpl.asStateFlow()
override val qsView: Flow<View> = _qsImpl.map { it?.view }.filterNotNull()
+ // Same config changes as in FragmentHostManager
+ private val interestingChanges =
+ InterestingConfigChanges(
+ ActivityInfo.CONFIG_FONT_SCALE or
+ ActivityInfo.CONFIG_LOCALE or
+ ActivityInfo.CONFIG_ASSETS_PATHS
+ )
+
init {
applicationScope.launch {
- state.sample(_isCustomizing, ::Pair).collect { (state, customizing) ->
- _qsImpl.value?.apply {
- if (state != QSSceneAdapter.State.QS && customizing) {
- this@apply.closeCustomizerImmediately()
+ launch {
+ state.sample(_isCustomizing, ::Pair).collect { (state, customizing) ->
+ _qsImpl.value?.apply {
+ if (state != QSSceneAdapter.State.QS && customizing) {
+ this@apply.closeCustomizerImmediately()
+ }
+ applyState(state)
+ }
+ }
+ }
+ launch {
+ configurationInteractor.configurationValues.collect { config ->
+ if (interestingChanges.applyNewConfig(config)) {
+ // Assumption: The context is always the same and with the same theme.
+ // If colors change they will be reflected as attributes in the theme.
+ qsImpl.value?.view?.let { inflate(it.context) }
+ } else {
+ qsImpl.value?.onConfigurationChanged(config)
+ qsImpl.value?.view?.dispatchConfigurationChanged(config)
}
- applyState(state)
}
}
}
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 e5e1e8445e94..8a900ece2750 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
@@ -16,7 +16,10 @@
package com.android.systemui.qs.ui.viewmodel
+import androidx.lifecycle.LifecycleOwner
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.qs.FooterActionsController
+import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
import com.android.systemui.qs.ui.adapter.QSSceneAdapter
import com.android.systemui.scene.shared.model.Direction
import com.android.systemui.scene.shared.model.SceneKey
@@ -24,6 +27,7 @@ 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.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel
+import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject
import kotlinx.coroutines.flow.map
@@ -35,6 +39,8 @@ constructor(
val shadeHeaderViewModel: ShadeHeaderViewModel,
val qsSceneAdapter: QSSceneAdapter,
val notifications: NotificationsPlaceholderViewModel,
+ private val footerActionsViewModelFactory: FooterActionsViewModel.Factory,
+ private val footerActionsController: FooterActionsController,
) {
val destinationScenes =
qsSceneAdapter.isCustomizing.map { customizing ->
@@ -47,4 +53,13 @@ constructor(
)
}
}
+
+ private val footerActionsControllerInitialized = AtomicBoolean(false)
+
+ fun getFooterActionsViewModel(lifecycleOwner: LifecycleOwner): FooterActionsViewModel {
+ if (footerActionsControllerInitialized.compareAndSet(false, true)) {
+ footerActionsController.init()
+ }
+ return footerActionsViewModelFactory.create(lifecycleOwner)
+ }
}
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 c8c134a9474a..563a3fe9fc7f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSImplTest.java
@@ -35,6 +35,7 @@ import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
import android.annotation.Nullable;
@@ -47,6 +48,7 @@ import android.view.Display;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import android.widget.FrameLayout;
import androidx.lifecycle.Lifecycle;
import androidx.test.filters.SmallTest;
@@ -63,6 +65,7 @@ import com.android.systemui.qs.footer.ui.binder.FooterActionsViewBinder;
import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel;
import com.android.systemui.qs.logging.QSLogger;
import com.android.systemui.res.R;
+import com.android.systemui.scene.shared.flag.SceneContainerFlags;
import com.android.systemui.settings.FakeDisplayTracker;
import com.android.systemui.shade.transition.LargeScreenShadeInterpolator;
import com.android.systemui.statusbar.CommandQueue;
@@ -111,7 +114,8 @@ public class QSImplTest extends SysuiTestCase {
@Mock private FooterActionsViewBinder mFooterActionsViewBinder;
@Mock private LargeScreenShadeInterpolator mLargeScreenShadeInterpolator;
@Mock private FeatureFlagsClassic mFeatureFlags;
- private View mQsView;
+ @Mock private SceneContainerFlags mSceneContainerFlags;
+ private ViewGroup mQsView;
private final CommandQueue mCommandQueue =
new CommandQueue(mContext, new FakeDisplayTracker(mContext));
@@ -121,6 +125,9 @@ public class QSImplTest extends SysuiTestCase {
@Before
public void setup() {
+ MockitoAnnotations.initMocks(this);
+ when(mSceneContainerFlags.isEnabled()).thenReturn(false);
+
mUnderTest = instantiate();
mUnderTest.onComponentCreated(mQsComponent, null);
@@ -487,9 +494,24 @@ public class QSImplTest extends SysuiTestCase {
verify(mQSAnimator).setOnKeyguard(true);
}
- private QSImpl instantiate() {
- MockitoAnnotations.initMocks(this);
+ @Test
+ public void testSceneContainerFlagsEnabled_FooterActionsRemoved_controllerNotStarted() {
+ when(mSceneContainerFlags.isEnabled()).thenReturn(true);
+ clearInvocations(
+ mFooterActionsViewBinder, mFooterActionsViewModel, mFooterActionsViewModelFactory);
+ QSImpl other = instantiate();
+
+ other.onComponentCreated(mQsComponent, null);
+ assertThat((View) other.getView().findViewById(R.id.qs_footer_actions)).isNull();
+ verifyZeroInteractions(
+ mFooterActionsViewModel,
+ mFooterActionsViewBinder,
+ mFooterActionsViewModelFactory
+ );
+ }
+
+ private QSImpl instantiate() {
setupQsComponent();
setUpViews();
setUpInflater();
@@ -514,7 +536,8 @@ public class QSImplTest extends SysuiTestCase {
mFooterActionsViewModelFactory,
mFooterActionsViewBinder,
mLargeScreenShadeInterpolator,
- mFeatureFlags);
+ mFeatureFlags,
+ mSceneContainerFlags);
}
private void setUpOther() {
@@ -533,14 +556,23 @@ public class QSImplTest extends SysuiTestCase {
}
private void setUpViews() {
- mQsView = spy(new View(mContext));
+ mQsView = spy(new FrameLayout(mContext));
when(mQsComponent.getRootView()).thenReturn(mQsView);
- when(mQsView.findViewById(R.id.expanded_qs_scroll_view))
+
+ when(mQSPanelScrollView.findViewById(R.id.expanded_qs_scroll_view))
.thenReturn(mQSPanelScrollView);
- when(mQsView.findViewById(R.id.header)).thenReturn(mHeader);
- when(mQsView.findViewById(android.R.id.edit)).thenReturn(new View(mContext));
- when(mQsView.findViewById(R.id.qs_footer_actions)).thenAnswer(
- invocation -> new FooterActionsViewBinder().create(mContext));
+ mQsView.addView(mQSPanelScrollView);
+
+ when(mHeader.findViewById(R.id.header)).thenReturn(mHeader);
+ mQsView.addView(mHeader);
+
+ View customizer = new View(mContext);
+ customizer.setId(android.R.id.edit);
+ mQsView.addView(customizer);
+
+ View footerActionsView = new FooterActionsViewBinder().create(mContext);
+ footerActionsView.setId(R.id.qs_footer_actions);
+ mQsView.addView(footerActionsView);
}
private void setUpInflater() {