diff options
12 files changed, 544 insertions, 26 deletions
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt index 9d689fc25b23..33a630c8086a 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt @@ -25,6 +25,7 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.clipScrollableContainer import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -42,6 +43,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.windowsizeclass.WindowHeightSizeClass import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect @@ -67,6 +69,7 @@ import com.android.compose.animation.scene.UserActionResult import com.android.compose.animation.scene.animateSceneFloatAsState import com.android.compose.modifiers.padding import com.android.compose.modifiers.thenIf +import com.android.compose.windowsizeclass.LocalWindowSizeClass import com.android.systemui.battery.BatteryMeterViewController import com.android.systemui.common.ui.compose.windowinsets.CutoutLocation import com.android.systemui.common.ui.compose.windowinsets.LocalDisplayCutout @@ -235,6 +238,10 @@ private fun SceneScope.SingleShade( val shouldPunchHoleBehindScrim = layoutState.isTransitioningBetween(Scenes.Gone, Scenes.Shade) || layoutState.isTransitioningBetween(Scenes.Lockscreen, Scenes.Shade) + // Media is visible and we are in landscape on a small height screen + val mediaInRow = + isMediaVisible && + LocalWindowSizeClass.current.heightSizeClass == WindowHeightSizeClass.Compact Box( modifier = @@ -274,22 +281,39 @@ private fun SceneScope.SingleShade( createBatteryMeterViewController = createBatteryMeterViewController, statusBarIconController = statusBarIconController, ) - Box(Modifier.element(QuickSettings.Elements.QuickQuickSettings)) { - QuickSettings( - viewModel.qsSceneAdapter, - { viewModel.qsSceneAdapter.qqsHeight }, - isSplitShade = false, - squishiness = { tileSquishiness }, + + val content: @Composable (Modifier) -> Unit = { modifier -> + Box( + Modifier.element(QuickSettings.Elements.QuickQuickSettings) + .then(modifier) + ) { + QuickSettings( + viewModel.qsSceneAdapter, + { viewModel.qsSceneAdapter.qqsHeight }, + isSplitShade = false, + squishiness = { tileSquishiness }, + ) + } + + MediaCarousel( + isVisible = isMediaVisible, + mediaHost = mediaHost, + modifier = Modifier.fillMaxWidth().then(modifier), + carouselController = mediaCarouselController, ) } - MediaCarousel( - isVisible = isMediaVisible, - mediaHost = mediaHost, - modifier = Modifier.fillMaxWidth(), - carouselController = mediaCarouselController, - ) - + if (!mediaInRow) { + content(Modifier) + } else { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + content(Modifier.weight(1f)) + } + } Spacer(modifier = Modifier.height(16.dp)) } }, diff --git a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java index b34b3701528b..e77bd03b8af2 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java +++ b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java @@ -485,6 +485,11 @@ public class PagedTileLayout extends ViewPager implements QSTileLayout { } @Override + public int getMinRows() { + return mMinRows; + } + + @Override public boolean setMaxColumns(int maxColumns) { mMaxColumns = maxColumns; boolean changed = false; @@ -497,6 +502,11 @@ public class PagedTileLayout extends ViewPager implements QSTileLayout { return changed; } + @Override + public int getMaxColumns() { + return mMaxColumns; + } + /** * Set the amount of excess space that we gave this view compared to the actual available * height. This is because this view is in a scrollview. diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java index 00757b7bd51a..9c8c17bb1ca0 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java @@ -42,6 +42,7 @@ import com.android.internal.widget.RemeasuringLinearLayout; import com.android.systemui.plugins.qs.QSTile; import com.android.systemui.qs.logging.QSLogger; import com.android.systemui.res.R; +import com.android.systemui.scene.shared.flag.SceneContainerFlag; import com.android.systemui.settings.brightness.BrightnessSliderController; import com.android.systemui.tuner.TunerService; import com.android.systemui.tuner.TunerService.Tunable; @@ -441,7 +442,7 @@ public class QSPanel extends LinearLayout implements Tunable { } private boolean needsDynamicRowsAndColumns() { - return true; + return !SceneContainerFlag.isEnabled(); } private void switchAllContentToParent(ViewGroup parent, QSTileLayout newLayout) { @@ -634,8 +635,7 @@ public class QSPanel extends LinearLayout implements Tunable { switchAllContentToParent(newParent, mTileLayout); reAttachMediaHost(mediaHostView, horizontal); if (needsDynamicRowsAndColumns()) { - mTileLayout.setMinRows(horizontal ? 2 : 1); - mTileLayout.setMaxColumns(horizontal ? 2 : 4); + setColumnRowLayout(horizontal); } updateMargins(mediaHostView); if (mHorizontalLinearLayout != null) { @@ -644,6 +644,11 @@ public class QSPanel extends LinearLayout implements Tunable { } } + void setColumnRowLayout(boolean withMedia) { + mTileLayout.setMinRows(withMedia ? 2 : 1); + mTileLayout.setMaxColumns(withMedia ? 2 : 4); + } + private void updateMargins(ViewGroup mediaHostView) { updateMediaHostContentMargins(mediaHostView); updateHorizontalLinearLayoutMargins(); @@ -736,6 +741,8 @@ public class QSPanel extends LinearLayout implements Tunable { return false; } + int getMinRows(); + /** * Sets the max number of columns to show * @@ -747,6 +754,8 @@ public class QSPanel extends LinearLayout implements Tunable { return false; } + int getMaxColumns(); + /** * Sets the expansion value and proposedTranslation to panel. */ diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java index e24caf19a14b..f76183ed2d74 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java @@ -30,6 +30,7 @@ import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.UiEventLogger; import com.android.systemui.dump.DumpManager; import com.android.systemui.haptics.qs.QSLongPressEffect; +import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor; import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager; import com.android.systemui.media.controls.ui.view.MediaHost; import com.android.systemui.media.controls.ui.view.MediaHostState; @@ -46,10 +47,13 @@ import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; import com.android.systemui.statusbar.policy.SplitShadeStateController; import com.android.systemui.tuner.TunerService; +import kotlinx.coroutines.flow.StateFlow; + import javax.inject.Inject; import javax.inject.Named; import javax.inject.Provider; + /** * Controller for {@link QSPanel}. */ @@ -72,6 +76,8 @@ public class QSPanelController extends QSPanelControllerBase<QSPanel> { private final BrightnessSliderController.Factory mBrightnessSliderControllerFactory; private final BrightnessController.Factory mBrightnessControllerFactory; + protected final MediaCarouselInteractor mMediaCarouselInteractor; + private View.OnTouchListener mTileLayoutTouchListener = new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { @@ -94,7 +100,8 @@ public class QSPanelController extends QSPanelControllerBase<QSPanel> { FalsingManager falsingManager, StatusBarKeyguardViewManager statusBarKeyguardViewManager, SplitShadeStateController splitShadeStateController, - Provider<QSLongPressEffect> longPRessEffectProvider) { + Provider<QSLongPressEffect> longPRessEffectProvider, + MediaCarouselInteractor mediaCarouselInteractor) { super(view, qsHost, qsCustomizerController, usingMediaPlayer, mediaHost, metricsLogger, uiEventLogger, qsLogger, dumpManager, splitShadeStateController, longPRessEffectProvider); @@ -113,6 +120,7 @@ public class QSPanelController extends QSPanelControllerBase<QSPanel> { mStatusBarKeyguardViewManager = statusBarKeyguardViewManager; mLastDensity = view.getResources().getConfiguration().densityDpi; mSceneContainerEnabled = SceneContainerFlag.isEnabled(); + mMediaCarouselInteractor = mediaCarouselInteractor; } @Override @@ -126,6 +134,11 @@ public class QSPanelController extends QSPanelControllerBase<QSPanel> { } @Override + StateFlow<Boolean> getMediaVisibleFlow() { + return mMediaCarouselInteractor.getHasAnyMediaOrRecommendation(); + } + + @Override protected void onViewAttached() { super.onViewAttached(); diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java index bd1d709b086a..3b5cc61057e6 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java @@ -41,13 +41,17 @@ import com.android.systemui.qs.customize.QSCustomizerController; import com.android.systemui.qs.external.CustomTile; import com.android.systemui.qs.logging.QSLogger; import com.android.systemui.qs.tileimpl.QSTileViewImpl; +import com.android.systemui.scene.shared.flag.SceneContainerFlag; import com.android.systemui.statusbar.policy.SplitShadeStateController; import com.android.systemui.util.ViewController; import com.android.systemui.util.animation.DisappearParameters; +import com.android.systemui.util.kotlin.JavaAdapterKt; import kotlin.Unit; import kotlin.jvm.functions.Function1; +import kotlinx.coroutines.flow.StateFlow; + import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collection; @@ -58,6 +62,7 @@ import java.util.stream.Collectors; import javax.inject.Provider; + /** * Controller for QSPanel views. * @@ -95,6 +100,13 @@ public abstract class QSPanelControllerBase<T extends QSPanel> extends ViewContr private boolean mDestroyed = false; + private boolean mMediaVisibleFromInteractor; + + private final Consumer<Boolean> mMediaOrRecommendationVisibleConsumer = mediaVisible -> { + mMediaVisibleFromInteractor = mediaVisible; + setLayoutForMediaInScene(); + }; + @VisibleForTesting protected final QSPanel.OnConfigurationChangedListener mOnConfigurationChangedListener = new QSPanel.OnConfigurationChangedListener() { @@ -117,7 +129,11 @@ public abstract class QSPanelControllerBase<T extends QSPanel> extends ViewContr /* newScreenLayout= */ mLastScreenLayout, /* containerName= */ mView.getDumpableTag()); - switchTileLayoutIfNeeded(); + if (SceneContainerFlag.isEnabled()) { + setLayoutForMediaInScene(); + } else { + switchTileLayoutIfNeeded(); + } onConfigurationChanged(); if (previousSplitShadeState != mShouldUseSplitNotificationShade) { onSplitShadeChanged(mShouldUseSplitNotificationShade); @@ -175,6 +191,9 @@ public abstract class QSPanelControllerBase<T extends QSPanel> extends ViewContr mView.initialize(mQSLogger, mUsingMediaPlayer); mQSLogger.logAllTilesChangeListening(mView.isListening(), mView.getDumpableTag(), ""); mHost.addCallback(mQSHostCallback); + if (SceneContainerFlag.isEnabled()) { + registerForMediaInteractorChanges(); + } } /** @@ -209,17 +228,32 @@ public abstract class QSPanelControllerBase<T extends QSPanel> extends ViewContr mQsTileRevealController.setExpansion(mRevealExpansion); } - mMediaHost.addVisibilityChangeListener(mMediaHostVisibilityListener); + if (!SceneContainerFlag.isEnabled()) { + mMediaHost.addVisibilityChangeListener(mMediaHostVisibilityListener); + } mView.addOnConfigurationChangedListener(mOnConfigurationChangedListener); setTiles(); mLastOrientation = getResources().getConfiguration().orientation; mLastScreenLayout = getResources().getConfiguration().screenLayout; mQSLogger.logOnViewAttached(mLastOrientation, mView.getDumpableTag()); + if (SceneContainerFlag.isEnabled()) { + setLayoutForMediaInScene(); + } switchTileLayout(true); mDumpManager.registerDumpable(mView.getDumpableTag(), this); } + private void registerForMediaInteractorChanges() { + JavaAdapterKt.collectFlow( + mView, + getMediaVisibleFlow(), + mMediaOrRecommendationVisibleConsumer + ); + } + + abstract StateFlow<Boolean> getMediaVisibleFlow(); + @Override protected void onViewDetached() { mQSLogger.logOnViewDetached(mLastOrientation, mView.getDumpableTag()); @@ -436,6 +470,11 @@ public abstract class QSPanelControllerBase<T extends QSPanel> extends ViewContr return false; } + void setLayoutForMediaInScene() { + boolean withMedia = shouldUseHorizontalInScene(); + mView.setColumnRowLayout(withMedia); + } + /** * Update the way the media disappears based on if we're using the horizontal layout */ @@ -476,6 +515,16 @@ public abstract class QSPanelControllerBase<T extends QSPanel> extends ViewContr == Configuration.SCREENLAYOUT_LONG_YES; } + boolean shouldUseHorizontalInScene() { + if (mShouldUseSplitNotificationShade) { + return false; + } + return mMediaVisibleFromInteractor + && mLastOrientation == Configuration.ORIENTATION_LANDSCAPE + && (mLastScreenLayout & Configuration.SCREENLAYOUT_LONG_MASK) + == Configuration.SCREENLAYOUT_LONG_YES; + } + private void logTiles() { for (int i = 0; i < mRecords.size(); i++) { QSTile tile = mRecords.get(i).tile; diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java index 6cda740dd1a8..f207b1de3cba 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java @@ -26,6 +26,7 @@ import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.UiEventLogger; import com.android.systemui.dump.DumpManager; import com.android.systemui.haptics.qs.QSLongPressEffect; +import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor; import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager; import com.android.systemui.media.controls.ui.view.MediaHost; import com.android.systemui.plugins.qs.QSTile; @@ -36,6 +37,8 @@ import com.android.systemui.res.R; import com.android.systemui.statusbar.policy.SplitShadeStateController; import com.android.systemui.util.leak.RotationUtils; +import kotlinx.coroutines.flow.StateFlow; + import java.util.ArrayList; import java.util.List; @@ -43,12 +46,15 @@ import javax.inject.Inject; import javax.inject.Named; import javax.inject.Provider; + /** Controller for {@link QuickQSPanel}. */ @QSScope public class QuickQSPanelController extends QSPanelControllerBase<QuickQSPanel> { private final Provider<Boolean> mUsingCollapsedLandscapeMediaProvider; + private final MediaCarouselInteractor mMediaCarouselInteractor; + @Inject QuickQSPanelController(QuickQSPanel view, QSHost qsHost, QSCustomizerController qsCustomizerController, @@ -58,12 +64,14 @@ public class QuickQSPanelController extends QSPanelControllerBase<QuickQSPanel> Provider<Boolean> usingCollapsedLandscapeMediaProvider, MetricsLogger metricsLogger, UiEventLogger uiEventLogger, QSLogger qsLogger, DumpManager dumpManager, SplitShadeStateController splitShadeStateController, - Provider<QSLongPressEffect> longPressEffectProvider + Provider<QSLongPressEffect> longPressEffectProvider, + MediaCarouselInteractor mediaCarouselInteractor ) { super(view, qsHost, qsCustomizerController, usingMediaPlayer, mediaHost, metricsLogger, uiEventLogger, qsLogger, dumpManager, splitShadeStateController, longPressEffectProvider); mUsingCollapsedLandscapeMediaProvider = usingCollapsedLandscapeMediaProvider; + mMediaCarouselInteractor = mediaCarouselInteractor; } @Override @@ -74,6 +82,11 @@ public class QuickQSPanelController extends QSPanelControllerBase<QuickQSPanel> mMediaHost.init(MediaHierarchyManager.LOCATION_QQS); } + @Override + StateFlow<Boolean> getMediaVisibleFlow() { + return mMediaCarouselInteractor.getHasActiveMediaOrRecommendation(); + } + private void updateMediaExpansion() { int rotation = getRotation(); boolean isLandscape = rotation == RotationUtils.ROTATION_LANDSCAPE diff --git a/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java b/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java index dcb9288f74be..ef44e5fb8757 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java +++ b/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java @@ -12,6 +12,7 @@ import android.view.accessibility.AccessibilityNodeInfo; import android.widget.TextView; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import com.android.internal.logging.UiEventLogger; import com.android.systemui.FontSizeUtils; @@ -97,12 +98,24 @@ public class TileLayout extends ViewGroup implements QSTileLayout { return false; } + @VisibleForTesting + @Override + public int getMinRows() { + return mMinRows; + } + @Override public boolean setMaxColumns(int maxColumns) { mMaxColumns = maxColumns; return updateColumns(); } + @VisibleForTesting + @Override + public int getMaxColumns() { + return mMaxColumns; + } + public void addTile(TileRecord tile) { mRecords.add(tile); tile.tile.setListening(this, mListening); diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseSceneContainerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseSceneContainerTest.kt new file mode 100644 index 000000000000..07ec38e6ae6c --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseSceneContainerTest.kt @@ -0,0 +1,300 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs + +import android.content.res.Configuration +import android.content.res.Resources +import android.testing.TestableLooper.RunWithLooper +import android.view.ViewTreeObserver +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.internal.logging.MetricsLogger +import com.android.internal.logging.UiEventLogger +import com.android.internal.logging.testing.UiEventLoggerFake +import com.android.systemui.SysuiTestCase +import com.android.systemui.dump.DumpManager +import com.android.systemui.flags.EnableSceneContainer +import com.android.systemui.haptics.qs.QSLongPressEffect +import com.android.systemui.haptics.qs.qsLongPressEffect +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope +import com.android.systemui.lifecycle.InstantTaskExecutorRule +import com.android.systemui.media.controls.ui.view.MediaHost +import com.android.systemui.qs.customize.QSCustomizerController +import com.android.systemui.qs.logging.QSLogger +import com.android.systemui.statusbar.policy.SplitShadeStateController +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import javax.inject.Provider +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.MockitoAnnotations +import org.mockito.kotlin.clearInvocations +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +@RunWith(AndroidJUnit4::class) +@SmallTest +@RunWithLooper +@OptIn(ExperimentalCoroutinesApi::class) +@EnableSceneContainer +class QSPanelControllerBaseSceneContainerTest : SysuiTestCase() { + + @Rule @JvmField val mInstantTaskExecutor = InstantTaskExecutorRule() + + private val kosmos = testKosmos() + + @Mock private lateinit var qsPanel: QSPanel + @Mock private lateinit var qsHost: QSHost + @Mock private lateinit var qsCustomizerController: QSCustomizerController + @Mock private lateinit var metricsLogger: MetricsLogger + private val uiEventLogger = UiEventLoggerFake() + @Mock private lateinit var qsLogger: QSLogger + private val dumpManager = DumpManager() + @Mock private lateinit var tileLayout: PagedTileLayout + @Mock private lateinit var resources: Resources + private val configuration = Configuration() + @Mock private lateinit var viewTreeObserver: ViewTreeObserver + @Mock private lateinit var mediaHost: MediaHost + + private var isSplitShade = false + private val splitShadeStateController = + object : SplitShadeStateController { + override fun shouldUseSplitNotificationShade(resources: Resources): Boolean { + return isSplitShade + } + } + private val longPressEffectProvider: Provider<QSLongPressEffect> = Provider { + kosmos.qsLongPressEffect + } + + private val mediaVisible = MutableStateFlow(false) + + private lateinit var underTest: TestableQSPanelControllerBase + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + allowTestableLooperAsMainThread() + Dispatchers.setMain(kosmos.testDispatcher) + + whenever(qsPanel.isAttachedToWindow).thenReturn(true) + whenever(qsPanel.orCreateTileLayout).thenReturn(tileLayout) + whenever(qsPanel.tileLayout).thenReturn(tileLayout) + whenever(qsPanel.resources).thenReturn(resources) + whenever(qsPanel.viewTreeObserver).thenReturn(viewTreeObserver) + whenever(qsHost.tiles).thenReturn(emptyList()) + whenever(resources.configuration).thenReturn(configuration) + + underTest = createUnderTest() + underTest.init() + } + + @After + fun tearDown() { + disallowTestableLooperAsMainThread() + Dispatchers.resetMain() + } + + @Test + fun configurationChange_onlySplitShadeConfigChanges_horizontalInSceneUpdated() = + with(kosmos) { + testScope.runTest { + clearInvocations(qsPanel) + + mediaVisible.value = true + runCurrent() + isSplitShade = false + configuration.orientation = Configuration.ORIENTATION_LANDSCAPE + configuration.screenLayout = Configuration.SCREENLAYOUT_LONG_YES + underTest.mOnConfigurationChangedListener.onConfigurationChange(configuration) + + assertThat(underTest.shouldUseHorizontalInScene()).isTrue() + verify(qsPanel).setColumnRowLayout(true) + clearInvocations(qsPanel) + + isSplitShade = true + underTest.mOnConfigurationChangedListener.onConfigurationChange(configuration) + + assertThat(underTest.shouldUseHorizontalInScene()).isFalse() + verify(qsPanel).setColumnRowLayout(false) + } + } + + @Test + fun configurationChange_shouldUseHorizontalInSceneInLongDevices() = + with(kosmos) { + testScope.runTest { + clearInvocations(qsPanel) + + mediaVisible.value = true + runCurrent() + isSplitShade = false + // When device is rotated to landscape and is long + configuration.orientation = Configuration.ORIENTATION_LANDSCAPE + configuration.screenLayout = Configuration.SCREENLAYOUT_LONG_YES + underTest.mOnConfigurationChangedListener.onConfigurationChange(configuration) + + // Then the layout changes + assertThat(underTest.shouldUseHorizontalInScene()).isTrue() + verify(qsPanel).setColumnRowLayout(true) + clearInvocations(qsPanel) + + // When device changes to not-long + configuration.screenLayout = Configuration.SCREENLAYOUT_LONG_NO + underTest.mOnConfigurationChangedListener.onConfigurationChange(configuration) + + // Then the layout changes back + assertThat(underTest.shouldUseHorizontalInScene()).isFalse() + verify(qsPanel).setColumnRowLayout(false) + } + } + + @Test + fun configurationChange_horizontalInScene_onlyInLandscape() = + with(kosmos) { + testScope.runTest { + clearInvocations(qsPanel) + + mediaVisible.value = true + runCurrent() + isSplitShade = false + + // When device is rotated to landscape and is long + configuration.orientation = Configuration.ORIENTATION_LANDSCAPE + configuration.screenLayout = Configuration.SCREENLAYOUT_LONG_YES + underTest.mOnConfigurationChangedListener.onConfigurationChange(configuration) + + // Then the layout changes + assertThat(underTest.shouldUseHorizontalInScene()).isTrue() + verify(qsPanel).setColumnRowLayout(true) + clearInvocations(qsPanel) + + // When it is rotated back to portrait + configuration.orientation = Configuration.ORIENTATION_PORTRAIT + underTest.mOnConfigurationChangedListener.onConfigurationChange(configuration) + + // Then the layout changes back + assertThat(underTest.shouldUseHorizontalInScene()).isFalse() + verify(qsPanel).setColumnRowLayout(false) + } + } + + @Test + fun changeMediaVisible_changesHorizontalInScene() = + with(kosmos) { + testScope.runTest { + mediaVisible.value = false + runCurrent() + isSplitShade = false + configuration.orientation = Configuration.ORIENTATION_LANDSCAPE + configuration.screenLayout = Configuration.SCREENLAYOUT_LONG_YES + underTest.mOnConfigurationChangedListener.onConfigurationChange(configuration) + + assertThat(underTest.shouldUseHorizontalInScene()).isFalse() + clearInvocations(qsPanel) + + mediaVisible.value = true + runCurrent() + + assertThat(underTest.shouldUseHorizontalInScene()).isTrue() + verify(qsPanel).setColumnRowLayout(true) + } + } + + @Test + fun startFromMediaHorizontalLong_shouldUseHorizontal() = + with(kosmos) { + testScope.runTest { + mediaVisible.value = true + runCurrent() + isSplitShade = false + configuration.orientation = Configuration.ORIENTATION_LANDSCAPE + configuration.screenLayout = Configuration.SCREENLAYOUT_LONG_YES + + underTest = createUnderTest() + underTest.init() + runCurrent() + + assertThat(underTest.shouldUseHorizontalInScene()).isTrue() + verify(qsPanel).setColumnRowLayout(true) + } + } + + private fun createUnderTest(): TestableQSPanelControllerBase { + return TestableQSPanelControllerBase( + qsPanel, + qsHost, + qsCustomizerController, + mediaHost, + metricsLogger, + uiEventLogger, + qsLogger, + dumpManager, + splitShadeStateController, + longPressEffectProvider, + mediaVisible, + ) + } + + private class TestableQSPanelControllerBase( + view: QSPanel, + qsHost: QSHost, + qsCustomizerController: QSCustomizerController, + mediaHost: MediaHost, + metricsLogger: MetricsLogger, + uiEventLogger: UiEventLogger, + qsLogger: QSLogger, + dumpManager: DumpManager, + splitShadeStateController: SplitShadeStateController, + longPressEffectProvider: Provider<QSLongPressEffect>, + private val mediaVisibleFlow: StateFlow<Boolean> + ) : + QSPanelControllerBase<QSPanel>( + view, + qsHost, + qsCustomizerController, + /* usingMediaPlayer= */ false, + mediaHost, + metricsLogger, + uiEventLogger, + qsLogger, + dumpManager, + splitShadeStateController, + longPressEffectProvider + ) { + + init { + whenever(view.dumpableTag).thenReturn(hashCode().toString()) + } + override fun getMediaVisibleFlow(): StateFlow<Boolean> { + return mediaVisibleFlow + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java index 7c5bc7196b24..225adab04ff0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java @@ -17,9 +17,13 @@ package com.android.systemui.qs; import static com.android.systemui.Flags.FLAG_QUICK_SETTINGS_VISUAL_HAPTICS_LONGPRESS; +import static com.android.systemui.flags.SceneContainerFlagParameterizationKt.parameterizeSceneContainerFlag; import static com.google.common.truth.Truth.assertThat; +import static kotlinx.coroutines.flow.FlowKt.asStateFlow; +import static kotlinx.coroutines.flow.StateFlowKt.MutableStateFlow; + import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; @@ -37,9 +41,10 @@ import android.content.res.Configuration; import android.content.res.Resources; import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; -import android.testing.AndroidTestingRunner; +import android.platform.test.flag.junit.FlagsParameterization; import android.testing.TestableLooper.RunWithLooper; import android.view.ContextThemeWrapper; +import android.view.ViewTreeObserver; import androidx.test.filters.SmallTest; @@ -49,18 +54,26 @@ import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.logging.testing.UiEventLoggerFake; import com.android.systemui.SysuiTestCase; import com.android.systemui.dump.DumpManager; +import com.android.systemui.flags.DisableSceneContainer; import com.android.systemui.haptics.qs.QSLongPressEffect; import com.android.systemui.kosmos.KosmosJavaAdapter; +import com.android.systemui.lifecycle.InstantTaskExecutorRule; import com.android.systemui.media.controls.ui.view.MediaHost; import com.android.systemui.plugins.qs.QSTile; import com.android.systemui.qs.customize.QSCustomizerController; import com.android.systemui.qs.logging.QSLogger; import com.android.systemui.qs.tileimpl.QSTileImpl; import com.android.systemui.res.R; +import com.android.systemui.scene.shared.flag.SceneContainerFlag; import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController; import com.android.systemui.util.animation.DisappearParameters; +import kotlinx.coroutines.flow.MutableStateFlow; +import kotlinx.coroutines.flow.StateFlow; + +import org.junit.After; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -73,11 +86,22 @@ import java.util.List; import javax.inject.Provider; -@RunWith(AndroidTestingRunner.class) +import platform.test.runner.parameterized.ParameterizedAndroidJunit4; +import platform.test.runner.parameterized.Parameters; + +@RunWith(ParameterizedAndroidJunit4.class) @RunWithLooper @SmallTest public class QSPanelControllerBaseTest extends SysuiTestCase { + @Rule + public final InstantTaskExecutorRule mInstantTaskExecutor = new InstantTaskExecutorRule(); + + @Parameters(name = "{0}") + public static List<FlagsParameterization> getParams() { + return parameterizeSceneContainerFlag(); + } + private final KosmosJavaAdapter mKosmos = new KosmosJavaAdapter(this); @Mock private QSPanel mQSPanel; @@ -109,10 +133,13 @@ public class QSPanelControllerBaseTest extends SysuiTestCase { Configuration mConfiguration; @Mock Runnable mHorizontalLayoutListener; + @Mock + private ViewTreeObserver mViewTreeObserver; + private TestableLongPressEffectProvider mLongPressEffectProvider = new TestableLongPressEffectProvider(); - private QSPanelControllerBase<QSPanel> mController; + private TestableQSPanelControllerBase mController; /** Implementation needed to ensure we have a reflectively-available class name. */ private class TestableQSPanelControllerBase extends QSPanelControllerBase<QSPanel> { @@ -120,15 +147,27 @@ public class QSPanelControllerBaseTest extends SysuiTestCase { QSCustomizerController qsCustomizerController, MediaHost mediaHost, MetricsLogger metricsLogger, UiEventLogger uiEventLogger, QSLogger qsLogger, DumpManager dumpManager) { - super(view, host, qsCustomizerController, true, mediaHost, metricsLogger, uiEventLogger, + super(view, host, qsCustomizerController, usingMediaPlayer(), + mediaHost, metricsLogger, uiEventLogger, qsLogger, dumpManager, new ResourcesSplitShadeStateController(), mLongPressEffectProvider); } + private MutableStateFlow<Boolean> mMediaVisible = MutableStateFlow(false); + @Override protected QSTileRevealController createTileRevealController() { return mQSTileRevealController; } + + @Override + StateFlow<Boolean> getMediaVisibleFlow() { + return asStateFlow(mMediaVisible); + } + + void setMediaVisible(boolean visible) { + mMediaVisible.tryEmit(visible); + } } private class TestableLongPressEffectProvider implements Provider<QSLongPressEffect> { @@ -142,16 +181,24 @@ public class QSPanelControllerBaseTest extends SysuiTestCase { } } + public QSPanelControllerBaseTest(FlagsParameterization flags) { + super(); + mSetFlagsRule.setFlagsParameterization(flags); + } + @Before public void setup() throws Exception { MockitoAnnotations.initMocks(this); + allowTestableLooperAsMainThread(); + when(mQSPanel.isAttachedToWindow()).thenReturn(true); when(mQSPanel.getDumpableTag()).thenReturn("QSPanel"); when(mQSPanel.openPanelEvent()).thenReturn(QSEvent.QS_PANEL_EXPANDED); when(mQSPanel.closePanelEvent()).thenReturn(QSEvent.QS_PANEL_COLLAPSED); when(mQSPanel.getOrCreateTileLayout()).thenReturn(mPagedTileLayout); when(mQSPanel.getTileLayout()).thenReturn(mPagedTileLayout); + when(mQSPanel.getViewTreeObserver()).thenReturn(mViewTreeObserver); when(mQSTile.getTileSpec()).thenReturn("dnd"); when(mQSHost.getTiles()).thenReturn(Collections.singleton(mQSTile)); when(mQSTileRevealControllerFactory.create(any(), any())) @@ -174,6 +221,11 @@ public class QSPanelControllerBaseTest extends SysuiTestCase { reset(mQSTileRevealController); } + @After + public void tearDown() { + disallowTestableLooperAsMainThread(); + } + @Test public void testSetRevealExpansion_preAttach() { mController.onViewDetached(); @@ -269,6 +321,7 @@ public class QSPanelControllerBaseTest extends SysuiTestCase { @Test + @DisableSceneContainer public void testShouldUseHorizontalLayout_falseForSplitShade() { mConfiguration.orientation = Configuration.ORIENTATION_LANDSCAPE; mConfiguration.screenLayout = Configuration.SCREENLAYOUT_LONG_YES; @@ -294,6 +347,7 @@ public class QSPanelControllerBaseTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void testChangeConfiguration_shouldUseHorizontalLayoutInLandscape_true() { when(mMediaHost.getVisible()).thenReturn(true); mController.setUsingHorizontalLayoutChangeListener(mHorizontalLayoutListener); @@ -317,6 +371,7 @@ public class QSPanelControllerBaseTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void testChangeConfiguration_shouldUseHorizontalLayoutInLongDevices_true() { when(mMediaHost.getVisible()).thenReturn(true); mController.setUsingHorizontalLayoutChangeListener(mHorizontalLayoutListener); @@ -353,6 +408,7 @@ public class QSPanelControllerBaseTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void configurationChange_onlySplitShadeConfigChanges_horizontalLayoutStatusUpdated() { // Preconditions for horizontal layout when(mMediaHost.getVisible()).thenReturn(true); @@ -514,4 +570,8 @@ public class QSPanelControllerBaseTest extends SysuiTestCase { assertThat(mController.mRecords).isEmpty(); } + + private boolean usingMediaPlayer() { + return !SceneContainerFlag.isEnabled(); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt index e50320df2740..545d19dd771e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt @@ -10,6 +10,7 @@ import com.android.internal.logging.UiEventLogger import com.android.systemui.SysuiTestCase import com.android.systemui.dump.DumpManager import com.android.systemui.haptics.qs.QSLongPressEffect +import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor import com.android.systemui.media.controls.ui.view.MediaHost import com.android.systemui.media.controls.ui.view.MediaHostState import com.android.systemui.plugins.FalsingManager @@ -17,6 +18,7 @@ import com.android.systemui.plugins.qs.QSTile import com.android.systemui.qs.customize.QSCustomizerController import com.android.systemui.qs.logging.QSLogger import com.android.systemui.res.R +import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.settings.brightness.BrightnessController import com.android.systemui.settings.brightness.BrightnessSliderController import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager @@ -63,6 +65,9 @@ class QSPanelControllerTest : SysuiTestCase() { @Mock private lateinit var configuration: Configuration @Mock private lateinit var pagedTileLayout: PagedTileLayout @Mock private lateinit var longPressEffectProvider: Provider<QSLongPressEffect> + @Mock private lateinit var mediaCarouselInteractor: MediaCarouselInteractor + + private val usingMediaPlayer: Boolean by lazy { !SceneContainerFlag.isEnabled } private lateinit var controller: QSPanelController private val testableResources: TestableResources = mContext.orCreateTestableResources @@ -88,7 +93,7 @@ class QSPanelControllerTest : SysuiTestCase() { tunerService, qsHost, qsCustomizerController, - /* usingMediaPlayer= */ true, + /* usingMediaPlayer= */ usingMediaPlayer, mediaHost, qsTileRevealControllerFactory, dumpManager, @@ -101,6 +106,7 @@ class QSPanelControllerTest : SysuiTestCase() { statusBarKeyguardViewManager, ResourcesSplitShadeStateController(), longPressEffectProvider, + mediaCarouselInteractor, ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt index 5c6ed70c85a6..e2a4d6727e80 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt @@ -275,6 +275,19 @@ class QSPanelTest : SysuiTestCase() { ViewUtils.detachView(panel) } + @Test + fun setRowColumnLayout() { + qsPanel.setColumnRowLayout(/* withMedia= */ false) + + assertThat(qsPanel.tileLayout!!.minRows).isEqualTo(1) + assertThat(qsPanel.tileLayout!!.maxColumns).isEqualTo(4) + + qsPanel.setColumnRowLayout(/* withMedia= */ true) + + assertThat(qsPanel.tileLayout!!.minRows).isEqualTo(2) + assertThat(qsPanel.tileLayout!!.maxColumns).isEqualTo(2) + } + private infix fun View.isLeftOf(other: View): Boolean { val rect = Rect() getBoundsOnScreen(rect) diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt index 1eb0a51bcaf6..fee4b534d8dd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt @@ -25,6 +25,7 @@ import com.android.internal.logging.testing.UiEventLoggerFake import com.android.systemui.SysuiTestCase import com.android.systemui.dump.DumpManager import com.android.systemui.haptics.qs.QSLongPressEffect +import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor import com.android.systemui.media.controls.ui.view.MediaHost import com.android.systemui.media.controls.ui.view.MediaHostState import com.android.systemui.plugins.qs.QSTile @@ -62,6 +63,10 @@ class QuickQSPanelControllerTest : SysuiTestCase() { @Mock private lateinit var tileLayout: TileLayout @Captor private lateinit var captor: ArgumentCaptor<QSPanel.OnConfigurationChangedListener> @Mock private lateinit var longPressEffectProvider: Provider<QSLongPressEffect> + @Mock private lateinit var mediaCarouselInteractor: MediaCarouselInteractor + + private val usingMediaPlayer: Boolean + get() = false private val uiEventLogger = UiEventLoggerFake() private val dumpManager = DumpManager() @@ -86,7 +91,7 @@ class QuickQSPanelControllerTest : SysuiTestCase() { quickQSPanel, qsHost, qsCustomizerController, - /* usingMediaPlayer = */ false, + usingMediaPlayer, mediaHost, { usingCollapsedLandscapeMedia }, metricsLogger, @@ -94,6 +99,7 @@ class QuickQSPanelControllerTest : SysuiTestCase() { qsLogger, dumpManager, longPressEffectProvider, + mediaCarouselInteractor, ) controller.init() @@ -163,6 +169,7 @@ class QuickQSPanelControllerTest : SysuiTestCase() { qsLogger: QSLogger, dumpManager: DumpManager, longPressEffectProvider: Provider<QSLongPressEffect>, + mediaCarouselInteractor: MediaCarouselInteractor, ) : QuickQSPanelController( view, @@ -177,6 +184,7 @@ class QuickQSPanelControllerTest : SysuiTestCase() { dumpManager, ResourcesSplitShadeStateController(), longPressEffectProvider, + mediaCarouselInteractor ) { private var rotation = RotationUtils.ROTATION_NONE |