From 2e0f351d3508c9d4dadc206d5cc056afa8adb1ce Mon Sep 17 00:00:00 2001 From: Fabian Kozynski Date: Wed, 29 Jul 2020 16:08:43 -0400 Subject: [PATCH] Add guts to media player on long press Adds the following: * Button for accessing settings * Button for dismissing player (similar path to when a package is uninstalled) Guts will close automatically if: * QS is collapsed * Media carousel changes pages Also, flattened the view hierarchy to support animations between states. Test: manual Test: atest com.android.systemui.media Bug: 156036025 Change-Id: I340e0b37393573f81a3bf12d5e453eccf5982473 Merged-In: I340e0b37393573f81a3bf12d5e453eccf5982473 (cherry picked from commit 429360fb399f4aa2bb51cbcdf76deb0f319e130f) --- packages/SystemUI/res/layout/media_view.xml | 98 ++++++++++++++++- .../res/layout/qs_media_panel_options.xml | 61 ----------- packages/SystemUI/res/values/strings.xml | 2 +- .../systemui/media/MediaCarouselController.kt | 8 +- .../media/MediaCarouselScrollHandler.kt | 2 + .../systemui/media/MediaControlPanel.java | 58 +++++++++- .../systemui/media/MediaDataManager.kt | 22 ++-- .../systemui/media/MediaHierarchyManager.kt | 7 ++ .../systemui/media/MediaViewController.kt | 69 +++++++++++- .../systemui/media/PlayerViewHolder.kt | 35 +++++- .../NotificationPanelViewController.java | 1 + .../systemui/media/MediaControlPanelTest.kt | 101 +++++++++++++++++- .../systemui/media/MediaDataManagerTest.kt | 14 +++ .../media/MediaHierarchyManagerTest.kt | 7 ++ 14 files changed, 400 insertions(+), 85 deletions(-) delete mode 100644 packages/SystemUI/res/layout/qs_media_panel_options.xml diff --git a/packages/SystemUI/res/layout/media_view.xml b/packages/SystemUI/res/layout/media_view.xml index 3c641afea0d66..ed870f8bb2ef2 100644 --- a/packages/SystemUI/res/layout/media_view.xml +++ b/packages/SystemUI/res/layout/media_view.xml @@ -210,8 +210,98 @@ android:layout_width="@dimen/qs_media_icon_size" android:layout_height="@dimen/qs_media_icon_size" /> - - + + + + + + + + + + + + + + + + + + + diff --git a/packages/SystemUI/res/layout/qs_media_panel_options.xml b/packages/SystemUI/res/layout/qs_media_panel_options.xml deleted file mode 100644 index e72c0e85fb260..0000000000000 --- a/packages/SystemUI/res/layout/qs_media_panel_options.xml +++ /dev/null @@ -1,61 +0,0 @@ - - - - - - - - - diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 28c1b02fc415e..f875ecb9354e2 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2822,7 +2822,7 @@ Hide the current session. - Hide + Dismiss Resume diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt index 3a84123582213..15c60921cca53 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt @@ -151,7 +151,7 @@ class MediaCarouselController @Inject constructor( pageIndicator = mediaFrame.requireViewById(R.id.media_page_indicator) mediaCarouselScrollHandler = MediaCarouselScrollHandler(mediaCarousel, pageIndicator, executor, mediaManager::onSwipeToDismiss, this::updatePageIndicatorLocation, - falsingManager) + this::closeGuts, falsingManager) isRtl = context.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL inflateSettingsButton() mediaContent = mediaCarousel.requireViewById(R.id.media_carousel) @@ -470,6 +470,12 @@ class MediaCarouselController @Inject constructor( } } + fun closeGuts() { + mediaPlayers.values.forEach { + it.closeGuts(true) + } + } + /** * Update the size of the carousel, remeasuring it if necessary. */ diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt index 3096908aca215..77cac5023db37 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt @@ -56,6 +56,7 @@ class MediaCarouselScrollHandler( private val mainExecutor: DelayableExecutor, private val dismissCallback: () -> Unit, private var translationChangedListener: () -> Unit, + private val closeGuts: () -> Unit, private val falsingManager: FalsingManager ) { /** @@ -452,6 +453,7 @@ class MediaCarouselScrollHandler( val nowScrolledIn = scrollIntoCurrentMedia != 0 if (newIndex != activeMediaIndex || wasScrolledIn != nowScrolledIn) { activeMediaIndex = newIndex + closeGuts() updatePlayerVisibilities() } val relativeLocation = activeMediaIndex.toFloat() + if (playerWidthPlusPadding > 0) diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java index 3fc162ead6d1e..e55678dc986bc 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java +++ b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java @@ -16,6 +16,8 @@ package com.android.systemui.media; +import static android.provider.Settings.ACTION_MEDIA_CONTROLS_SETTINGS; + import android.app.PendingIntent; import android.content.Context; import android.content.Intent; @@ -45,6 +47,7 @@ import com.android.settingslib.widget.AdaptiveIcon; import com.android.systemui.R; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.plugins.ActivityStarter; +import com.android.systemui.statusbar.phone.KeyguardDismissUtil; import com.android.systemui.util.animation.TransitionLayout; import java.util.List; @@ -52,6 +55,8 @@ import java.util.concurrent.Executor; import javax.inject.Inject; +import dagger.Lazy; + /** * A view controller used for Media Playback. */ @@ -59,6 +64,8 @@ public class MediaControlPanel { private static final String TAG = "MediaControlPanel"; private static final float DISABLED_ALPHA = 0.38f; + private static final Intent SETTINGS_INTENT = new Intent(ACTION_MEDIA_CONTROLS_SETTINGS); + // Button IDs for QS controls static final int[] ACTION_IDS = { R.id.action0, @@ -78,6 +85,8 @@ public class MediaControlPanel { private MediaViewController mMediaViewController; private MediaSession.Token mToken; private MediaController mController; + private KeyguardDismissUtil mKeyguardDismissUtil; + private Lazy mMediaDataManagerLazy; private int mBackgroundColor; private int mAlbumArtSize; private int mAlbumArtRadius; @@ -93,12 +102,15 @@ public class MediaControlPanel { @Inject public MediaControlPanel(Context context, @Background Executor backgroundExecutor, ActivityStarter activityStarter, MediaViewController mediaViewController, - SeekBarViewModel seekBarViewModel) { + SeekBarViewModel seekBarViewModel, Lazy lazyMediaDataManager, + KeyguardDismissUtil keyguardDismissUtil) { mContext = context; mBackgroundExecutor = backgroundExecutor; mActivityStarter = activityStarter; mSeekBarViewModel = seekBarViewModel; mMediaViewController = mediaViewController; + mMediaDataManagerLazy = lazyMediaDataManager; + mKeyguardDismissUtil = keyguardDismissUtil; loadDimens(); mViewOutlineProvider = new ViewOutlineProvider() { @@ -174,6 +186,21 @@ public class MediaControlPanel { mSeekBarViewModel.getProgress().observeForever(mSeekBarObserver); mSeekBarViewModel.attachTouchHandlers(vh.getSeekBar()); mMediaViewController.attach(player); + + mViewHolder.getPlayer().setOnLongClickListener(v -> { + if (!mMediaViewController.isGutsVisible()) { + mMediaViewController.openGuts(); + return true; + } else { + return false; + } + }); + mViewHolder.getCancel().setOnClickListener(v -> { + closeGuts(); + }); + mViewHolder.getSettings().setOnClickListener(v -> { + mActivityStarter.startActivity(SETTINGS_INTENT, true /* dismissShade */); + }); } /** @@ -205,6 +232,7 @@ public class MediaControlPanel { PendingIntent clickIntent = data.getClickIntent(); if (clickIntent != null) { mViewHolder.getPlayer().setOnClickListener(v -> { + if (mMediaViewController.isGutsVisible()) return; mActivityStarter.postStartActivityDismissingKeyguard(clickIntent); }); } @@ -329,14 +357,38 @@ public class MediaControlPanel { final MediaController controller = getController(); mBackgroundExecutor.execute(() -> mSeekBarViewModel.updateController(controller)); - // Set up long press menu - // TODO: b/156036025 bring back media guts + // Dismiss + mViewHolder.getDismiss().setOnClickListener(v -> { + if (data.getNotificationKey() != null) { + closeGuts(); + mKeyguardDismissUtil.executeWhenUnlocked(() -> { + mMediaDataManagerLazy.get().dismissMediaData(data.getNotificationKey(), + MediaViewController.GUTS_ANIMATION_DURATION + 100); + return true; + }, /* requiresShadeOpen */ true); + } else { + Log.w(TAG, "Dismiss media with null notification. Token uid=" + + data.getToken().getUid()); + } + }); // TODO: We don't need to refresh this state constantly, only if the state actually changed // to something which might impact the measurement mMediaViewController.refreshState(); } + /** + * Close the guts for this player. + * @param immediate {@code true} if it should be closed without animation + */ + public void closeGuts(boolean immediate) { + mMediaViewController.closeGuts(immediate); + } + + private void closeGuts() { + closeGuts(false); + } + @UiThread private Drawable scaleDrawable(Icon icon) { if (icon == null) { diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt index b3277737f3976..64ba5f7cc4833 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt @@ -48,6 +48,7 @@ import com.android.systemui.statusbar.notification.MediaNotificationProcessor import com.android.systemui.statusbar.notification.row.HybridGroupManager import com.android.systemui.util.Assert import com.android.systemui.util.Utils +import com.android.systemui.util.concurrency.DelayableExecutor import java.io.FileDescriptor import java.io.IOException import java.io.PrintWriter @@ -89,7 +90,7 @@ fun isMediaNotification(sbn: StatusBarNotification): Boolean { class MediaDataManager( private val context: Context, @Background private val backgroundExecutor: Executor, - @Main private val foregroundExecutor: Executor, + @Main private val foregroundExecutor: DelayableExecutor, private val mediaControllerFactory: MediaControllerFactory, private val broadcastDispatcher: BroadcastDispatcher, dumpManager: DumpManager, @@ -106,7 +107,7 @@ class MediaDataManager( constructor( context: Context, @Background backgroundExecutor: Executor, - @Main foregroundExecutor: Executor, + @Main foregroundExecutor: DelayableExecutor, mediaControllerFactory: MediaControllerFactory, dumpManager: DumpManager, broadcastDispatcher: BroadcastDispatcher, @@ -182,10 +183,7 @@ class MediaDataManager( val listenersCopy = listeners.toSet() val toRemove = mediaEntries.filter { it.value.packageName == packageName } toRemove.forEach { - mediaEntries.remove(it.key) - listenersCopy.forEach { listener -> - listener.onMediaDataRemoved(it.key) - } + removeEntry(it.key, listenersCopy) } } @@ -267,6 +265,18 @@ class MediaDataManager( } } + private fun removeEntry(key: String, listenersCopy: Set) { + mediaEntries.remove(key) + listenersCopy.forEach { + it.onMediaDataRemoved(key) + } + } + + fun dismissMediaData(key: String, delay: Long) { + val listenersCopy = listeners.toSet() + foregroundExecutor.executeDelayed({ removeEntry(key, listenersCopy) }, delay) + } + private fun loadMediaDataInBgForResumption( userId: Int, desc: MediaDescription, diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt index fc33391a9ad1c..70f01d576a9c2 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt @@ -293,6 +293,13 @@ class MediaHierarchyManager @Inject constructor( return viewHost } + /** + * Close the guts in all players in [MediaCarouselController]. + */ + fun closeGuts() { + mediaCarouselController.closeGuts() + } + private fun createUniqueObjectHost(): UniqueObjectHostView { val viewHost = UniqueObjectHostView(context) viewHost.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener { diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt b/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt index 38817d7b579e2..92eeed46388d8 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt @@ -37,6 +37,11 @@ class MediaViewController @Inject constructor( private val mediaHostStatesManager: MediaHostStatesManager ) { + companion object { + @JvmField + val GUTS_ANIMATION_DURATION = 500L + } + /** * A listener when the current dimensions of the player change */ @@ -169,6 +174,12 @@ class MediaViewController @Inject constructor( */ val expandedLayout = ConstraintSet() + /** + * Whether the guts are visible for the associated player. + */ + var isGutsVisible = false + private set + init { collapsedLayout.load(context, R.xml.media_collapsed) expandedLayout.load(context, R.xml.media_expanded) @@ -189,6 +200,37 @@ class MediaViewController @Inject constructor( configurationController.removeCallback(configurationListener) } + /** + * Show guts with an animated transition. + */ + fun openGuts() { + if (isGutsVisible) return + isGutsVisible = true + animatePendingStateChange(GUTS_ANIMATION_DURATION, 0L) + setCurrentState(currentStartLocation, + currentEndLocation, + currentTransitionProgress, + applyImmediately = false) + } + + /** + * Close the guts for the associated player. + * + * @param immediate if `false`, it will animate the transition. + */ + @JvmOverloads + fun closeGuts(immediate: Boolean = false) { + if (!isGutsVisible) return + isGutsVisible = false + if (!immediate) { + animatePendingStateChange(GUTS_ANIMATION_DURATION, 0L) + } + setCurrentState(currentStartLocation, + currentEndLocation, + currentTransitionProgress, + applyImmediately = immediate) + } + private fun ensureAllMeasurements() { val mediaStates = mediaHostStatesManager.mediaHostStates for (entry in mediaStates) { @@ -202,6 +244,24 @@ class MediaViewController @Inject constructor( private fun constraintSetForExpansion(expansion: Float): ConstraintSet = if (expansion > 0) expandedLayout else collapsedLayout + /** + * Set the views to be showing/hidden based on the [isGutsVisible] for a given + * [TransitionViewState]. + */ + private fun setGutsViewState(viewState: TransitionViewState) { + PlayerViewHolder.controlsIds.forEach { id -> + viewState.widgetStates.get(id)?.let { state -> + // Make sure to use the unmodified state if guts are not visible + state.alpha = if (isGutsVisible) 0f else state.alpha + state.gone = if (isGutsVisible) true else state.gone + } + } + PlayerViewHolder.gutsIds.forEach { id -> + viewState.widgetStates.get(id)?.alpha = if (isGutsVisible) 1f else 0f + viewState.widgetStates.get(id)?.gone = !isGutsVisible + } + } + /** * Obtain a new viewState for a given media state. This usually returns a cached state, but if * it's not available, it will recreate one by measuring, which may be expensive. @@ -211,7 +271,7 @@ class MediaViewController @Inject constructor( return null } // Only a subset of the state is relevant to get a valid viewState. Let's get the cachekey - var cacheKey = getKey(state, tmpKey) + var cacheKey = getKey(state, isGutsVisible, tmpKey) val viewState = viewStates[cacheKey] if (viewState != null) { // we already have cached this measurement, let's continue @@ -228,6 +288,7 @@ class MediaViewController @Inject constructor( constraintSetForExpansion(state.expansion), TransitionViewState()) + setGutsViewState(result) // We don't want to cache interpolated or null states as this could quickly fill up // our cache. We only cache the start and the end states since the interpolation // is cheap @@ -252,11 +313,12 @@ class MediaViewController @Inject constructor( return result } - private fun getKey(state: MediaHostState, result: CacheKey): CacheKey { + private fun getKey(state: MediaHostState, guts: Boolean, result: CacheKey): CacheKey { result.apply { heightMeasureSpec = state.measurementInput?.heightMeasureSpec ?: 0 widthMeasureSpec = state.measurementInput?.widthMeasureSpec ?: 0 expansion = state.expansion + gutsVisible = guts } return result } @@ -432,5 +494,6 @@ class MediaViewController @Inject constructor( private data class CacheKey( var widthMeasureSpec: Int = -1, var heightMeasureSpec: Int = -1, - var expansion: Float = 0.0f + var expansion: Float = 0.0f, + var gutsVisible: Boolean = false ) diff --git a/packages/SystemUI/src/com/android/systemui/media/PlayerViewHolder.kt b/packages/SystemUI/src/com/android/systemui/media/PlayerViewHolder.kt index 600fdc27ef89b..666a6038a8b68 100644 --- a/packages/SystemUI/src/com/android/systemui/media/PlayerViewHolder.kt +++ b/packages/SystemUI/src/com/android/systemui/media/PlayerViewHolder.kt @@ -59,6 +59,11 @@ class PlayerViewHolder private constructor(itemView: View) { val action3 = itemView.requireViewById(R.id.action3) val action4 = itemView.requireViewById(R.id.action4) + // Settings screen + val cancel = itemView.requireViewById(R.id.cancel) + val dismiss = itemView.requireViewById(R.id.dismiss) + val settings = itemView.requireViewById(R.id.settings) + init { (player.background as IlluminationDrawable).let { it.registerLightSource(seamless) @@ -67,6 +72,9 @@ class PlayerViewHolder private constructor(itemView: View) { it.registerLightSource(action2) it.registerLightSource(action3) it.registerLightSource(action4) + it.registerLightSource(cancel) + it.registerLightSource(dismiss) + it.registerLightSource(settings) } } @@ -83,9 +91,6 @@ class PlayerViewHolder private constructor(itemView: View) { } } - // Settings screen - val options = itemView.requireViewById(R.id.qs_media_controls_options) - companion object { /** * Creates a PlayerViewHolder. @@ -105,5 +110,29 @@ class PlayerViewHolder private constructor(itemView: View) { progressTimes.layoutDirection = View.LAYOUT_DIRECTION_LTR } } + + val controlsIds = setOf( + R.id.icon, + R.id.app_name, + R.id.album_art, + R.id.header_title, + R.id.header_artist, + R.id.media_seamless, + R.id.notification_media_progress_time, + R.id.media_progress_bar, + R.id.action0, + R.id.action1, + R.id.action2, + R.id.action3, + R.id.action4, + R.id.icon + ) + val gutsIds = setOf( + R.id.media_text, + R.id.remove_text, + R.id.cancel, + R.id.dismiss, + R.id.settings + ) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java index 64202d221b2d8..799e16cc6d8d4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java @@ -2612,6 +2612,7 @@ public class NotificationPanelViewController extends PanelViewController { super.onClosingFinished(); resetHorizontalPanelPosition(); setClosingWithAlphaFadeout(false); + mMediaHierarchyManager.closeGuts(); } private void setClosingWithAlphaFadeout(boolean closing) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt index c63781cb110a7..8a30b00e609d6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt @@ -16,6 +16,7 @@ package com.android.systemui.media +import android.content.Intent import android.content.res.ColorStateList import android.graphics.Color import android.graphics.drawable.GradientDrawable @@ -23,6 +24,7 @@ import android.graphics.drawable.RippleDrawable import android.media.MediaMetadata import android.media.session.MediaSession import android.media.session.PlaybackState +import android.provider.Settings.ACTION_MEDIA_CONTROLS_SETTINGS import android.testing.AndroidTestingRunner import android.testing.TestableLooper import android.view.View @@ -35,24 +37,31 @@ import android.widget.TextView import androidx.constraintlayout.widget.ConstraintSet import androidx.lifecycle.LiveData import androidx.test.filters.SmallTest -import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.statusbar.phone.KeyguardDismissUtil import com.android.systemui.util.animation.TransitionLayout import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.any import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat +import dagger.Lazy import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatchers +import org.mockito.ArgumentMatchers.anyLong import org.mockito.Mock +import org.mockito.Mockito.anyBoolean import org.mockito.Mockito.mock +import org.mockito.Mockito.never import org.mockito.Mockito.verify -import org.mockito.Mockito.`when` as whenever import org.mockito.junit.MockitoJUnit +import org.mockito.Mockito.`when` as whenever private const val KEY = "TEST_KEY" private const val APP = "APP" @@ -81,6 +90,8 @@ public class MediaControlPanelTest : SysuiTestCase() { @Mock private lateinit var seekBarViewModel: SeekBarViewModel @Mock private lateinit var seekBarData: LiveData @Mock private lateinit var mediaViewController: MediaViewController + @Mock private lateinit var keyguardDismissUtil: KeyguardDismissUtil + @Mock private lateinit var mediaDataManager: MediaDataManager @Mock private lateinit var expandedSet: ConstraintSet @Mock private lateinit var collapsedSet: ConstraintSet private lateinit var appIcon: ImageView @@ -100,6 +111,9 @@ public class MediaControlPanelTest : SysuiTestCase() { private lateinit var action2: ImageButton private lateinit var action3: ImageButton private lateinit var action4: ImageButton + private lateinit var settings: View + private lateinit var cancel: View + private lateinit var dismiss: View private lateinit var session: MediaSession private val device = MediaDeviceData(true, null, DEVICE_NAME) @@ -114,7 +128,7 @@ public class MediaControlPanelTest : SysuiTestCase() { whenever(mediaViewController.collapsedLayout).thenReturn(collapsedSet) player = MediaControlPanel(context, bgExecutor, activityStarter, mediaViewController, - seekBarViewModel) + seekBarViewModel, Lazy { mediaDataManager }, keyguardDismissUtil) whenever(seekBarViewModel.progress).thenReturn(seekBarData) // Mock out a view holder for the player to attach to. @@ -156,6 +170,12 @@ public class MediaControlPanelTest : SysuiTestCase() { whenever(holder.action3).thenReturn(action3) action4 = ImageButton(context) whenever(holder.action4).thenReturn(action4) + settings = View(context) + whenever(holder.settings).thenReturn(settings) + cancel = View(context) + whenever(holder.cancel).thenReturn(cancel) + dismiss = View(context) + whenever(holder.dismiss).thenReturn(dismiss) // Create media session val metadataBuilder = MediaMetadata.Builder().apply { @@ -254,4 +274,79 @@ public class MediaControlPanelTest : SysuiTestCase() { assertThat(seamlessText.getText()).isEqualTo(DEVICE_NAME) assertThat(seamless.isEnabled()).isFalse() } + + @Test + fun longClick_gutsClosed() { + player.attach(holder) + whenever(mediaViewController.isGutsVisible).thenReturn(false) + + val captor = ArgumentCaptor.forClass(View.OnLongClickListener::class.java) + verify(holder.player).setOnLongClickListener(captor.capture()) + + captor.value.onLongClick(holder.player) + verify(mediaViewController).openGuts() + } + + @Test + fun longClick_gutsOpen() { + player.attach(holder) + whenever(mediaViewController.isGutsVisible).thenReturn(true) + + val captor = ArgumentCaptor.forClass(View.OnLongClickListener::class.java) + verify(holder.player).setOnLongClickListener(captor.capture()) + + captor.value.onLongClick(holder.player) + verify(mediaViewController, never()).openGuts() + } + + @Test + fun cancelButtonClick_animation() { + player.attach(holder) + + cancel.callOnClick() + + verify(mediaViewController).closeGuts(false) + } + + @Test + fun settingsButtonClick() { + player.attach(holder) + + settings.callOnClick() + + val captor = ArgumentCaptor.forClass(Intent::class.java) + verify(activityStarter).startActivity(captor.capture(), eq(true)) + + assertThat(captor.value.action).isEqualTo(ACTION_MEDIA_CONTROLS_SETTINGS) + } + + @Test + fun dismissButtonClick() { + player.attach(holder) + val state = MediaData(USER_ID, true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(), + emptyList(), PACKAGE, session.getSessionToken(), null, null, true, null, + notificationKey = KEY) + player.bind(state) + + dismiss.callOnClick() + val captor = ArgumentCaptor.forClass(ActivityStarter.OnDismissAction::class.java) + verify(keyguardDismissUtil).executeWhenUnlocked(captor.capture(), anyBoolean()) + + captor.value.onDismiss() + verify(mediaDataManager).dismissMediaData(eq(KEY), anyLong()) + } + + @Test + fun dismissButtonClick_nullNotificationKey() { + player.attach(holder) + val state = MediaData(USER_ID, true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(), + emptyList(), PACKAGE, session.getSessionToken(), null, null, true, null) + player.bind(state) + + verify(keyguardDismissUtil, never()) + .executeWhenUnlocked( + any(ActivityStarter.OnDismissAction::class.java), + ArgumentMatchers.anyBoolean() + ) + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt index 59c2d0e86c568..3789e6ef1f65c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt @@ -217,6 +217,20 @@ class MediaDataManagerTest : SysuiTestCase() { assertThat(data.actions).hasSize(1) } + @Test + fun testDismissMedia_listenerCalled() { + val listener = mock(MediaDataManager.Listener::class.java) + mediaDataManager.addListener(listener) + mediaDataManager.onNotificationAdded(KEY, mediaNotification) + mediaDataManager.onMediaDataLoaded(KEY, oldKey = null, data = mock(MediaData::class.java)) + mediaDataManager.dismissMediaData(KEY, 0L) + + foregroundExecutor.advanceClockToLast() + foregroundExecutor.runAllReady() + + verify(listener).onMediaDataRemoved(eq(KEY)) + } + /** * Simple implementation of [MediaDataManager.Listener] for the test. * diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaHierarchyManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaHierarchyManagerTest.kt index 91c5ff8ee6270..d86dfa5fa5f73 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaHierarchyManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaHierarchyManagerTest.kt @@ -142,4 +142,11 @@ class MediaHierarchyManagerTest : SysuiTestCase() { verify(mediaCarouselController).onDesiredLocationChanged(ArgumentMatchers.anyInt(), any(MediaHostState::class.java), anyBoolean(), anyLong(), anyLong()) } + + @Test + fun testCloseGutsRelayToCarousel() { + mediaHiearchyManager.closeGuts() + + verify(mediaCarouselController).closeGuts() + } } \ No newline at end of file