diff options
| author | 2023-11-09 13:29:14 -0600 | |
|---|---|---|
| committer | 2023-11-29 11:06:48 -0600 | |
| commit | 5d7575c445ca3dc52a8b3746f60046aee28c0572 (patch) | |
| tree | 7c59c4235dac31a591f6aa2da2e60a0fb30f315a | |
| parent | f835f34db10a1f2194a079e66615bb12de7d29fe (diff) | |
Add media carousel to shade scene
Create a new composable, MediaCarousel, to handle placing the existing
media carousel view into the shade scene. ShadeScene replaces the existing
QQS media host location, but skips calculating view measurements in favor of\
the layout size provided by the scene framework
Bug: 296122467
Test: atest SystemUITests
Test: manual
Flag: ACONFIG com.android.systemui.media_in_scene_container DEVELOPMENT
Change-Id: If68e79fcfe71b6055708eac6250a58dfa0312561
12 files changed, 308 insertions, 21 deletions
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaCarousel.kt b/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaCarousel.kt new file mode 100644 index 000000000000..735c43356a40 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaCarousel.kt @@ -0,0 +1,50 @@ +/* + * 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.media.controls.ui.composable + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.viewinterop.AndroidView +import com.android.compose.animation.scene.ElementKey +import com.android.compose.animation.scene.SceneScope +import com.android.systemui.media.controls.ui.MediaCarouselController +import com.android.systemui.media.controls.ui.MediaHost +import com.android.systemui.util.animation.MeasurementInput + +private object MediaCarousel { + object Elements { + internal val Content = ElementKey("MediaCarouselContent") + } +} + +@Composable +fun SceneScope.MediaCarousel( + mediaHost: MediaHost, + modifier: Modifier = Modifier, + layoutWidth: Int, + layoutHeight: Int, + carouselController: MediaCarouselController, +) { + // Notify controller to size the carousel for the current space + mediaHost.measurementInput = MeasurementInput(layoutWidth, layoutHeight) + carouselController.setSceneContainerSize(layoutWidth, layoutHeight) + + AndroidView( + modifier = modifier.element(MediaCarousel.Elements.Content), + factory = { _ -> carouselController.mediaFrame }, + ) +} 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 2df151bc2d8e..fcb1619b4804 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 @@ -23,23 +23,34 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer 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.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.layout +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.unit.dp import com.android.compose.animation.scene.ElementKey import com.android.compose.animation.scene.SceneScope import com.android.systemui.battery.BatteryMeterViewController import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.media.controls.ui.MediaCarouselController +import com.android.systemui.media.controls.ui.MediaHost +import com.android.systemui.media.controls.ui.composable.MediaCarousel +import com.android.systemui.media.dagger.MediaModule.QUICK_QS_PANEL import com.android.systemui.notifications.ui.composable.NotificationStack import com.android.systemui.qs.ui.adapter.QSSceneAdapter import com.android.systemui.qs.ui.composable.QuickSettings +import com.android.systemui.res.R import com.android.systemui.scene.shared.model.Direction import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.shared.model.SceneModel @@ -49,7 +60,9 @@ import com.android.systemui.shade.ui.viewmodel.ShadeSceneViewModel import com.android.systemui.statusbar.phone.StatusBarIconController import com.android.systemui.statusbar.phone.StatusBarIconController.TintedIconManager import com.android.systemui.statusbar.phone.StatusBarLocation +import com.android.systemui.util.animation.MeasurementInput import javax.inject.Inject +import javax.inject.Named import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -59,6 +72,7 @@ import kotlinx.coroutines.flow.stateIn object Shade { object Elements { val QuickSettings = ElementKey("ShadeQuickSettings") + val MediaCarousel = ElementKey("ShadeMediaCarousel") val Scrim = ElementKey("ShadeScrim") val ScrimBackground = ElementKey("ShadeScrimBackground") } @@ -87,6 +101,8 @@ constructor( private val tintedIconManagerFactory: TintedIconManager.Factory, private val batteryMeterViewControllerFactory: BatteryMeterViewController.Factory, private val statusBarIconController: StatusBarIconController, + private val mediaCarouselController: MediaCarouselController, + @Named(QUICK_QS_PANEL) private val mediaHost: MediaHost, ) : ComposableScene { override val key = SceneKey.Shade @@ -108,6 +124,8 @@ constructor( createTintedIconManager = tintedIconManagerFactory::create, createBatteryMeterViewController = batteryMeterViewControllerFactory::create, statusBarIconController = statusBarIconController, + mediaCarouselController = mediaCarouselController, + mediaHost = mediaHost, modifier = modifier, ) @@ -127,8 +145,12 @@ private fun SceneScope.ShadeScene( createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager, createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController, statusBarIconController: StatusBarIconController, + mediaCarouselController: MediaCarouselController, + mediaHost: MediaHost, modifier: Modifier = Modifier, ) { + val layoutWidth = remember { mutableStateOf(0) } + Box(modifier.element(Shade.Elements.Scrim)) { Spacer( modifier = @@ -159,6 +181,34 @@ private fun SceneScope.ShadeScene( viewModel.qsSceneAdapter, QSSceneAdapter.State.QQS ) + + if (viewModel.isMediaVisible()) { + val mediaHeight = dimensionResource(R.dimen.qs_media_session_height_expanded) + MediaCarousel( + modifier = + Modifier.height(mediaHeight).fillMaxWidth().layout { measurable, constraints + -> + val placeable = measurable.measure(constraints) + + // Notify controller to size the carousel for the current space + mediaHost.measurementInput = + MeasurementInput(placeable.width, placeable.height) + mediaCarouselController.setSceneContainerSize( + placeable.width, + placeable.height + ) + + layout(placeable.width, placeable.height) { + placeable.placeRelative(0, 0) + } + }, + mediaHost = mediaHost, + layoutWidth = layoutWidth.value, + layoutHeight = with(LocalDensity.current) { mediaHeight.toPx() }.toInt(), + carouselController = mediaCarouselController, + ) + } + Spacer(modifier = Modifier.height(16.dp)) NotificationStack( viewModel = viewModel.notifications, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt index 18b7168098ab..9a748b962943 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt @@ -37,6 +37,8 @@ import com.android.systemui.flags.Flags import com.android.systemui.keyguard.ui.viewmodel.KeyguardLongPressViewModel import com.android.systemui.keyguard.ui.viewmodel.LockscreenSceneViewModel import com.android.systemui.log.table.TableLogBuffer +import com.android.systemui.media.controls.pipeline.MediaDataManager +import com.android.systemui.media.controls.ui.MediaHost import com.android.systemui.model.SysUiState import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest @@ -188,6 +190,9 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { private val qsFlexiglassAdapter = FakeQSSceneAdapter(inflateDelegate = { _, _ -> mock<View>() }) + @Mock private lateinit var mediaDataManager: MediaDataManager + @Mock private lateinit var mediaHost: MediaHost + @Before fun setUp() { MockitoAnnotations.initMocks(this) @@ -240,6 +245,8 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { shadeHeaderViewModel = shadeHeaderViewModel, qsSceneAdapter = qsFlexiglassAdapter, notifications = utils.notificationsPlaceholderViewModel(), + mediaDataManager = mediaDataManager, + mediaHost = mediaHost, ) utils.deviceEntryRepository.setUnlocked(false) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt index e2640af136a7..a2946cc90bee 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt @@ -23,6 +23,8 @@ import com.android.systemui.authentication.shared.model.AuthenticationMethodMode import com.android.systemui.coroutines.collectLastValue import com.android.systemui.flags.FakeFeatureFlagsClassic import com.android.systemui.flags.Flags +import com.android.systemui.media.controls.pipeline.MediaDataManager +import com.android.systemui.media.controls.ui.MediaHost import com.android.systemui.qs.ui.adapter.FakeQSSceneAdapter import com.android.systemui.scene.SceneTestUtils import com.android.systemui.scene.shared.model.SceneKey @@ -35,6 +37,7 @@ 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.util.mockito.mock +import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runCurrent @@ -42,6 +45,8 @@ import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.MockitoAnnotations @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @@ -83,8 +88,12 @@ class ShadeSceneViewModelTest : SysuiTestCase() { private lateinit var underTest: ShadeSceneViewModel + @Mock private lateinit var mediaDataManager: MediaDataManager + @Mock private lateinit var mediaHost: MediaHost + @Before fun setUp() { + MockitoAnnotations.initMocks(this) shadeHeaderViewModel = ShadeHeaderViewModel( applicationScope = testScope.backgroundScope, @@ -102,6 +111,8 @@ class ShadeSceneViewModelTest : SysuiTestCase() { shadeHeaderViewModel = shadeHeaderViewModel, qsSceneAdapter = qsFlexiglassAdapter, notifications = utils.notificationsPlaceholderViewModel(), + mediaDataManager = mediaDataManager, + mediaHost = mediaHost, ) } @@ -174,4 +185,20 @@ class ShadeSceneViewModelTest : SysuiTestCase() { assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) } + + @Test + fun hasActiveMedia_mediaVisible() = + testScope.runTest { + whenever(mediaDataManager.hasActiveMediaOrRecommendation()).thenReturn(true) + + assertThat(underTest.isMediaVisible()).isTrue() + } + + @Test + fun doesNotHaveActiveMedia_mediaNotVisible() = + testScope.runTest { + whenever(mediaDataManager.hasActiveMediaOrRecommendation()).thenReturn(false) + + assertThat(underTest.isMediaVisible()).isFalse() + } } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselController.kt index a2524709b8bc..9cdf85794088 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselController.kt @@ -271,6 +271,10 @@ constructor( private val isReorderingAllowed: Boolean get() = visualStabilityProvider.isReorderingAllowed + /** Size provided by the scene framework container */ + private var widthInSceneContainerPx = 0 + private var heightInSceneContainerPx = 0 + init { dumpManager.registerDumpable(TAG, this) mediaFrame = inflateMediaCarousel() @@ -581,6 +585,15 @@ constructor( } } + fun setSceneContainerSize(width: Int, height: Int) { + if (width == widthInSceneContainerPx && height == heightInSceneContainerPx) { + return + } + widthInSceneContainerPx = width + heightInSceneContainerPx = height + updatePlayers(recreateMedia = true) + } + private fun reorderAllPlayers( previousVisiblePlayerKey: MediaPlayerData.MediaSortKey?, key: String? = null @@ -638,6 +651,11 @@ constructor( .elementAtOrNull(mediaCarouselScrollHandler.visibleMediaIndex) if (existingPlayer == null) { val newPlayer = mediaControlPanelFactory.get() + if (mediaFlags.isSceneContainerEnabled()) { + newPlayer.mediaViewController.widthInSceneContainerPx = widthInSceneContainerPx + newPlayer.mediaViewController.heightInSceneContainerPx = + heightInSceneContainerPx + } newPlayer.attachPlayer( MediaViewHolder.create(LayoutInflater.from(context), mediaContent) ) diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java index cce4cda8af8a..04883c368b78 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java @@ -81,7 +81,6 @@ import com.android.internal.logging.InstanceId; import com.android.internal.widget.CachingIconView; import com.android.settingslib.widget.AdaptiveIcon; import com.android.systemui.ActivityIntentHelper; -import com.android.systemui.res.R; import com.android.systemui.animation.ActivityLaunchAnimator; import com.android.systemui.animation.GhostedViewLaunchAnimatorController; import com.android.systemui.bluetooth.BroadcastDialogController; @@ -102,6 +101,7 @@ import com.android.systemui.media.controls.models.recommendation.RecommendationV import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData; import com.android.systemui.media.controls.pipeline.MediaDataManager; import com.android.systemui.media.controls.util.MediaDataUtils; +import com.android.systemui.media.controls.util.MediaFlags; import com.android.systemui.media.controls.util.MediaUiEventLogger; import com.android.systemui.media.controls.util.SmallHash; import com.android.systemui.media.dialog.MediaOutputDialogFactory; @@ -109,6 +109,7 @@ import com.android.systemui.monet.ColorScheme; import com.android.systemui.monet.Style; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.FalsingManager; +import com.android.systemui.res.R; import com.android.systemui.shared.system.SysUiStatsLog; import com.android.systemui.statusbar.NotificationLockscreenUserManager; import com.android.systemui.statusbar.policy.KeyguardStateController; @@ -190,6 +191,7 @@ public class MediaControlPanel { @VisibleForTesting static final long TURBULENCE_NOISE_PLAY_DURATION = 7500L; private final SeekBarViewModel mSeekBarViewModel; + private final MediaFlags mMediaFlags; private SeekBarObserver mSeekBarObserver; protected final Executor mBackgroundExecutor; private final DelayableExecutor mMainExecutor; @@ -280,7 +282,8 @@ public class MediaControlPanel { NotificationLockscreenUserManager lockscreenUserManager, BroadcastDialogController broadcastDialogController, FeatureFlags featureFlags, - GlobalSettings globalSettings + GlobalSettings globalSettings, + MediaFlags mediaFlags ) { mContext = context; mBackgroundExecutor = backgroundExecutor; @@ -299,6 +302,7 @@ public class MediaControlPanel { mActivityIntentHelper = activityIntentHelper; mLockscreenUserManager = lockscreenUserManager; mBroadcastDialogController = broadcastDialogController; + mMediaFlags = mediaFlags; mSeekBarViewModel.setLogSeek(() -> { if (mPackageName != null && mInstanceId != null) { @@ -575,7 +579,10 @@ public class MediaControlPanel { // to something which might impact the measurement // State refresh interferes with the translation animation, only run it if it's not running. if (!mMetadataAnimationHandler.isRunning()) { - mMediaViewController.refreshState(); + // Don't refresh in scene framework, because it will calculate with invalid layout sizes + if (!mMediaFlags.isSceneContainerEnabled()) { + mMediaViewController.refreshState(); + } } // Turbulence noise @@ -805,7 +812,14 @@ public class MediaControlPanel { // Capture width & height from views in foreground for artwork scaling in background int width = mMediaViewHolder.getAlbumView().getMeasuredWidth(); int height = mMediaViewHolder.getAlbumView().getMeasuredHeight(); + if (mMediaFlags.isSceneContainerEnabled() && (width <= 0 || height <= 0)) { + // TODO(b/312714128): ensure we have a valid size before setting background + width = mMediaViewController.getWidthInSceneContainerPx(); + height = mMediaViewController.getHeightInSceneContainerPx(); + } + final int finalWidth = width; + final int finalHeight = height; mBackgroundExecutor.execute(() -> { // Album art ColorScheme mutableColorScheme = null; @@ -815,7 +829,8 @@ public class MediaControlPanel { WallpaperColors wallpaperColors = getWallpaperColor(artworkIcon); if (wallpaperColors != null) { mutableColorScheme = new ColorScheme(wallpaperColors, true, Style.CONTENT); - artwork = addGradientToPlayerAlbum(artworkIcon, mutableColorScheme, width, height); + artwork = addGradientToPlayerAlbum(artworkIcon, mutableColorScheme, finalWidth, + finalHeight); isArtworkBound = true; } else { // If there's no artwork, use colors from the app icon @@ -857,8 +872,10 @@ public class MediaControlPanel { TransitionDrawable transitionDrawable = new TransitionDrawable( new Drawable[]{mPrevArtwork, artwork}); - scaleTransitionDrawableLayer(transitionDrawable, 0, width, height); - scaleTransitionDrawableLayer(transitionDrawable, 1, width, height); + scaleTransitionDrawableLayer(transitionDrawable, 0, finalWidth, + finalHeight); + scaleTransitionDrawableLayer(transitionDrawable, 1, finalWidth, + finalHeight); transitionDrawable.setLayerGravity(0, Gravity.CENTER); transitionDrawable.setLayerGravity(1, Gravity.CENTER); transitionDrawable.setCrossFadeEnabled(true); diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt index 9d6e9b4f3ed5..35456d5ee472 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt @@ -43,6 +43,7 @@ import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dreams.DreamOverlayStateController import com.android.systemui.keyguard.WakefulnessLifecycle import com.android.systemui.media.controls.pipeline.MediaDataManager +import com.android.systemui.media.controls.util.MediaFlags import com.android.systemui.media.dream.MediaDreamComplication import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.res.R @@ -108,6 +109,7 @@ constructor( @Application private val coroutineScope: CoroutineScope, private val splitShadeStateController: SplitShadeStateController, private val logger: MediaViewLogger, + private val mediaFlags: MediaFlags, ) { /** Track the media player setting status on lock screen. */ @@ -215,6 +217,7 @@ constructor( } private val mediaHosts = arrayOfNulls<MediaHost>(LOCATION_COMMUNAL_HUB + 1) + /** * The last location where this view was at before going to the desired location. This is useful * for guided transitions. @@ -1041,6 +1044,17 @@ constructor( private fun updateHostAttachment() = traceSection("MediaHierarchyManager#updateHostAttachment") { + if (mediaFlags.isSceneContainerEnabled()) { + // No need to manage transition states - just update the desired location directly + logger.logMediaHostAttachment(desiredLocation) + mediaCarouselController.onDesiredLocationChanged( + desiredLocation = desiredLocation, + desiredHostState = getHost(desiredLocation), + animate = false, + ) + return + } + var newLocation = resolveLocationForFading() // Don't use the overlay when fading or when we don't have active media var canUseOverlay = !isCurrentlyFading() && hasActiveMediaOrRecommendation @@ -1124,6 +1138,7 @@ constructor( (!bypassController.bypassEnabled && (statusbarState == StatusBarState.KEYGUARD)) val location = when { + mediaFlags.isSceneContainerEnabled() -> desiredLocation dreamOverlayActive && dreamMediaComplicationActive -> LOCATION_DREAM_OVERLAY (qsExpansion > 0.0f || inSplitShade) && !onLockscreen -> LOCATION_QS qsExpansion > 0.4f && onLockscreen -> LOCATION_QS @@ -1282,7 +1297,7 @@ private annotation class TransformationType MediaHierarchyManager.LOCATION_QQS, MediaHierarchyManager.LOCATION_LOCKSCREEN, MediaHierarchyManager.LOCATION_DREAM_OVERLAY, - MediaHierarchyManager.LOCATION_COMMUNAL_HUB + MediaHierarchyManager.LOCATION_COMMUNAL_HUB, ] ) @Retention(AnnotationRetention.SOURCE) diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt index d277f32d5e54..a99c51c2e557 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt @@ -28,6 +28,7 @@ import com.android.systemui.media.controls.ui.MediaCarouselController.Companion. import com.android.systemui.media.controls.util.MediaFlags import com.android.systemui.res.R import com.android.systemui.statusbar.policy.ConfigurationController +import com.android.systemui.util.animation.MeasurementInput import com.android.systemui.util.animation.MeasurementOutput import com.android.systemui.util.animation.TransitionLayout import com.android.systemui.util.animation.TransitionLayoutController @@ -207,6 +208,10 @@ constructor( var isGutsVisible = false private set + /** Size provided by the scene framework container */ + var widthInSceneContainerPx = 0 + var heightInSceneContainerPx = 0 + init { mediaHostStatesManager.addController(this) layoutController.sizeChangedListener = { width: Int, height: Int -> @@ -420,6 +425,10 @@ constructor( state: MediaHostState?, isGutsAnimation: Boolean = false ): TransitionViewState? { + if (mediaFlags.isSceneContainerEnabled()) { + return obtainSceneContainerViewState() + } + if (state == null || state.measurementInput == null) { return null } @@ -670,6 +679,24 @@ constructor( refreshState() } + /** Get a view state based on the width and height set by the scene */ + private fun obtainSceneContainerViewState(): TransitionViewState? { + logger.logMediaSize("scene container", widthInSceneContainerPx, heightInSceneContainerPx) + + // Similar to obtainViewState: Let's create a new measurement + val result = + transitionLayout?.calculateViewState( + MeasurementInput(widthInSceneContainerPx, heightInSceneContainerPx), + expandedLayout, + TransitionViewState() + ) + result?.let { + // And then ensure the guts visibility is set correctly + setGutsViewState(it) + } + return result + } + /** * Retrieves the [TransitionViewState] and [MediaHostState] of a [@MediaLocation]. In the event * of [location] not being visible, [locationWhenHidden] will be used instead. @@ -681,6 +708,10 @@ constructor( */ private fun obtainViewStateForLocation(@MediaLocation location: Int): TransitionViewState? { val mediaHostState = mediaHostStatesManager.mediaHostStates[location] ?: return null + if (mediaFlags.isSceneContainerEnabled()) { + return obtainSceneContainerViewState() + } + val viewState = obtainViewState(mediaHostState) if (viewState != null) { // update the size of the viewstate for the location with the override @@ -708,6 +739,21 @@ constructor( /** Clear all existing measurements and refresh the state to match the view. */ fun refreshState() = traceSection("MediaViewController#refreshState") { + if (mediaFlags.isSceneContainerEnabled()) { + // We don't need to recreate measurements for scene container, since it's a known + // size. Just get the view state and update the layout controller + obtainSceneContainerViewState()?.let { + // Get scene container state, then setCurrentState + layoutController.setState( + state = it, + applyImmediately = true, + animate = false, + isGuts = false, + ) + } + return + } + // Let's clear all of our measurements and recreate them! viewStates.clear() if (firstRefresh) { diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt index 44232ffb3ad7..15747b9eb515 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt @@ -55,7 +55,7 @@ constructor( /** Check whether we allow remote media to generate resume controls */ fun isRemoteResumeAllowed() = featureFlags.isEnabled(Flags.MEDIA_REMOTE_RESUME) - /** Check whether to use flexiglass layout */ - fun isFlexiglassEnabled() = + /** Check whether to use scene framework */ + fun isSceneContainerEnabled() = sceneContainerFlags.isEnabled() && MediaInSceneContainerFlag.isEnabled } diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt index 0065db370b12..d0da945ca7d0 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt @@ -19,10 +19,16 @@ package com.android.systemui.shade.ui.viewmodel import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor +import com.android.systemui.media.controls.pipeline.MediaDataManager +import com.android.systemui.media.controls.ui.MediaHierarchyManager +import com.android.systemui.media.controls.ui.MediaHost +import com.android.systemui.media.controls.ui.MediaHostState +import com.android.systemui.media.dagger.MediaModule import com.android.systemui.qs.ui.adapter.QSSceneAdapter import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel import javax.inject.Inject +import javax.inject.Named import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -39,6 +45,8 @@ constructor( val qsSceneAdapter: QSSceneAdapter, val shadeHeaderViewModel: ShadeHeaderViewModel, val notifications: NotificationsPlaceholderViewModel, + val mediaDataManager: MediaDataManager, + @Named(MediaModule.QUICK_QS_PANEL) private val mediaHost: MediaHost, ) { /** The key of the scene we should switch to when swiping up. */ val upDestinationSceneKey: StateFlow<SceneKey> = @@ -74,4 +82,15 @@ constructor( else -> SceneKey.Lockscreen } } + + init { + mediaHost.expansion = MediaHostState.EXPANDED + mediaHost.showsOnlyActiveMedia = true + mediaHost.init(MediaHierarchyManager.LOCATION_QQS) + } + + fun isMediaVisible(): Boolean { + // TODO(b/296122467): handle updates to carousel visibility while scene is still visible + return mediaDataManager.hasActiveMediaOrRecommendation() + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt index d6e2e973b29d..2f35380e562b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt @@ -61,7 +61,6 @@ import androidx.test.filters.SmallTest import com.android.internal.logging.InstanceId import com.android.internal.widget.CachingIconView import com.android.systemui.ActivityIntentHelper -import com.android.systemui.res.R import com.android.systemui.SysuiTestCase import com.android.systemui.bluetooth.BroadcastDialogController import com.android.systemui.broadcast.BroadcastSender @@ -81,12 +80,14 @@ import com.android.systemui.media.controls.models.recommendation.RecommendationV import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData import com.android.systemui.media.controls.pipeline.EMPTY_SMARTSPACE_MEDIA_DATA import com.android.systemui.media.controls.pipeline.MediaDataManager +import com.android.systemui.media.controls.util.MediaFlags import com.android.systemui.media.controls.util.MediaUiEventLogger import com.android.systemui.media.dialog.MediaOutputDialogFactory import com.android.systemui.monet.ColorScheme import com.android.systemui.monet.Style import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.FalsingManager +import com.android.systemui.res.R import com.android.systemui.statusbar.NotificationLockscreenUserManager import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.surfaceeffects.ripple.MultiRippleView @@ -232,6 +233,7 @@ public class MediaControlPanelTest : SysuiTestCase() { this.set(Flags.UMO_TURBULENCE_NOISE, false) } @Mock private lateinit var globalSettings: GlobalSettings + @Mock private lateinit var mediaFlags: MediaFlags @JvmField @Rule val mockito = MockitoJUnit.rule() @@ -251,6 +253,7 @@ public class MediaControlPanelTest : SysuiTestCase() { .thenReturn(applicationInfo) whenever(packageManager.getApplicationLabel(any())).thenReturn(PACKAGE) context.setMockPackageManager(packageManager) + whenever(mediaFlags.isSceneContainerEnabled()).thenReturn(false) player = object : @@ -273,7 +276,8 @@ public class MediaControlPanelTest : SysuiTestCase() { lockscreenUserManager, broadcastDialogController, fakeFeatureFlag, - globalSettings + globalSettings, + mediaFlags, ) { override fun loadAnimator( animId: Int, @@ -1884,7 +1888,8 @@ public class MediaControlPanelTest : SysuiTestCase() { @Test fun bindRecommendation_listHasTooFewRecs_notDisplayed() { player.attachRecommendation(recommendationViewHolder) - val icon = Icon.createWithResource(context, com.android.settingslib.R.drawable.ic_1x_mobiledata) + val icon = + Icon.createWithResource(context, com.android.settingslib.R.drawable.ic_1x_mobiledata) val data = smartspaceData.copy( recommendations = @@ -1911,7 +1916,8 @@ public class MediaControlPanelTest : SysuiTestCase() { @Test fun bindRecommendation_listHasTooFewRecsWithIcons_notDisplayed() { player.attachRecommendation(recommendationViewHolder) - val icon = Icon.createWithResource(context, com.android.settingslib.R.drawable.ic_1x_mobiledata) + val icon = + Icon.createWithResource(context, com.android.settingslib.R.drawable.ic_1x_mobiledata) val data = smartspaceData.copy( recommendations = @@ -1955,7 +1961,8 @@ public class MediaControlPanelTest : SysuiTestCase() { val subtitle1 = "Subtitle1" val subtitle2 = "Subtitle2" val subtitle3 = "Subtitle3" - val icon = Icon.createWithResource(context, com.android.settingslib.R.drawable.ic_1x_mobiledata) + val icon = + Icon.createWithResource(context, com.android.settingslib.R.drawable.ic_1x_mobiledata) val data = smartspaceData.copy( @@ -1998,7 +2005,12 @@ public class MediaControlPanelTest : SysuiTestCase() { listOf( SmartspaceAction.Builder("id1", "") .setSubtitle("fake subtitle") - .setIcon(Icon.createWithResource(context, com.android.settingslib.R.drawable.ic_1x_mobiledata)) + .setIcon( + Icon.createWithResource( + context, + com.android.settingslib.R.drawable.ic_1x_mobiledata + ) + ) .setExtras(Bundle.EMPTY) .build() ) @@ -2013,7 +2025,8 @@ public class MediaControlPanelTest : SysuiTestCase() { useRealConstraintSets() player.attachRecommendation(recommendationViewHolder) - val icon = Icon.createWithResource(context, com.android.settingslib.R.drawable.ic_1x_mobiledata) + val icon = + Icon.createWithResource(context, com.android.settingslib.R.drawable.ic_1x_mobiledata) val data = smartspaceData.copy( recommendations = @@ -2047,7 +2060,8 @@ public class MediaControlPanelTest : SysuiTestCase() { useRealConstraintSets() player.attachRecommendation(recommendationViewHolder) - val icon = Icon.createWithResource(context, com.android.settingslib.R.drawable.ic_1x_mobiledata) + val icon = + Icon.createWithResource(context, com.android.settingslib.R.drawable.ic_1x_mobiledata) val data = smartspaceData.copy( recommendations = @@ -2086,7 +2100,12 @@ public class MediaControlPanelTest : SysuiTestCase() { listOf( SmartspaceAction.Builder("id1", "title1") .setSubtitle("") - .setIcon(Icon.createWithResource(context, com.android.settingslib.R.drawable.ic_1x_mobiledata)) + .setIcon( + Icon.createWithResource( + context, + com.android.settingslib.R.drawable.ic_1x_mobiledata + ) + ) .setExtras(Bundle.EMPTY) .build(), SmartspaceAction.Builder("id2", "title2") @@ -2096,7 +2115,12 @@ public class MediaControlPanelTest : SysuiTestCase() { .build(), SmartspaceAction.Builder("id3", "title3") .setSubtitle("") - .setIcon(Icon.createWithResource(context, com.android.settingslib.R.drawable.ic_3g_mobiledata)) + .setIcon( + Icon.createWithResource( + context, + com.android.settingslib.R.drawable.ic_3g_mobiledata + ) + ) .setExtras(Bundle.EMPTY) .build() ) @@ -2119,7 +2143,12 @@ public class MediaControlPanelTest : SysuiTestCase() { listOf( SmartspaceAction.Builder("id1", "") .setSubtitle("subtitle1") - .setIcon(Icon.createWithResource(context, com.android.settingslib.R.drawable.ic_1x_mobiledata)) + .setIcon( + Icon.createWithResource( + context, + com.android.settingslib.R.drawable.ic_1x_mobiledata + ) + ) .setExtras(Bundle.EMPTY) .build(), SmartspaceAction.Builder("id2", "") @@ -2129,7 +2158,12 @@ public class MediaControlPanelTest : SysuiTestCase() { .build(), SmartspaceAction.Builder("id3", "") .setSubtitle("subtitle3") - .setIcon(Icon.createWithResource(context, com.android.settingslib.R.drawable.ic_3g_mobiledata)) + .setIcon( + Icon.createWithResource( + context, + com.android.settingslib.R.drawable.ic_3g_mobiledata + ) + ) .setExtras(Bundle.EMPTY) .build() ) diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt index db7c9876f21b..73885f4d428d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt @@ -32,6 +32,7 @@ import com.android.systemui.controls.controller.ControlsControllerImplTest.Compa import com.android.systemui.dreams.DreamOverlayStateController import com.android.systemui.keyguard.WakefulnessLifecycle import com.android.systemui.media.controls.pipeline.MediaDataManager +import com.android.systemui.media.controls.util.MediaFlags import com.android.systemui.media.dream.MediaDreamComplication import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.res.R @@ -94,6 +95,7 @@ class MediaHierarchyManagerTest : SysuiTestCase() { @Mock private lateinit var dreamOverlayStateController: DreamOverlayStateController @Mock private lateinit var shadeInteractor: ShadeInteractor @Mock lateinit var logger: MediaViewLogger + @Mock private lateinit var mediaFlags: MediaFlags @Captor private lateinit var wakefullnessObserver: ArgumentCaptor<(WakefulnessLifecycle.Observer)> @Captor @@ -126,6 +128,7 @@ class MediaHierarchyManagerTest : SysuiTestCase() { whenever(mediaCarouselController.mediaFrame).thenReturn(mediaFrame) isQsBypassingShade = MutableStateFlow(false) whenever(shadeInteractor.isQsBypassingShade).thenReturn(isQsBypassingShade) + whenever(mediaFlags.isSceneContainerEnabled()).thenReturn(false) mediaHierarchyManager = MediaHierarchyManager( context, @@ -145,6 +148,7 @@ class MediaHierarchyManagerTest : SysuiTestCase() { testScope.backgroundScope, ResourcesSplitShadeStateController(), logger, + mediaFlags, ) verify(wakefulnessLifecycle).addObserver(wakefullnessObserver.capture()) verify(statusBarStateController).addCallback(statusBarCallback.capture()) |