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