diff options
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() { |