diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java index 9e498a8cc1cca..3fc162ead6d1e 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java +++ b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java @@ -57,6 +57,7 @@ import javax.inject.Inject; */ public class MediaControlPanel { private static final String TAG = "MediaControlPanel"; + private static final float DISABLED_ALPHA = 0.38f; // Button IDs for QS controls static final int[] ACTION_IDS = { @@ -267,6 +268,11 @@ public class MediaControlPanel { mViewHolder.getSeamless().setVisibility(seamlessVisibility); expandedSet.setVisibility(seamlessId, seamlessVisibility); collapsedSet.setVisibility(seamlessId, seamlessVisibility); + final float seamlessAlpha = data.getResumption() ? DISABLED_ALPHA : 1.0f; + expandedSet.setAlpha(seamlessId, seamlessAlpha); + collapsedSet.setAlpha(seamlessId, seamlessAlpha); + // Disable clicking on output switcher for resumption controls. + mViewHolder.getSeamless().setEnabled(!data.getResumption()); if (showFallback) { iconView.setImageDrawable(null); deviceName.setText(null); diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaData.kt b/packages/SystemUI/src/com/android/systemui/media/MediaData.kt index 0b0ffcede3af0..8c9cb1b240bfa 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaData.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaData.kt @@ -80,6 +80,11 @@ data class MediaData( * Action that should be performed to restart a non active session. */ var resumeAction: Runnable?, + /** + * Indicates that this player is a resumption player (ie. It only shows a play actions which + * will start the app and start playing). + */ + var resumption: Boolean = false, /** * Notification key for cancelling a media player after a timeout (when not using resumption.) */ diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt index 3c863a3fdfea2..416c81aee263e 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt @@ -323,7 +323,7 @@ class MediaDataManager( onMediaDataLoaded(packageName, null, MediaData(true, bgColor, appName, null, desc.subtitle, desc.title, artworkIcon, listOf(mediaAction), listOf(0), packageName, token, appIntent, device = null, active = false, - resumeAction = resumeAction, notificationKey = packageName, + resumeAction = resumeAction, resumption = true, notificationKey = packageName, hasCheckedForResume = true)) } } @@ -542,7 +542,7 @@ class MediaDataManager( val data = mediaEntries.remove(key)!! val resumeAction = getResumeMediaAction(data.resumeAction!!) val updated = data.copy(token = null, actions = listOf(resumeAction), - actionsToShowInCompact = listOf(0), active = false) + actionsToShowInCompact = listOf(0), active = false, resumption = true) mediaEntries.put(data.packageName, updated) // Notify listeners of "new" controls val listenersCopy = listeners.toSet() 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 a297f32b9c342..b7f317b387433 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt @@ -242,4 +242,15 @@ public class MediaControlPanelTest : SysuiTestCase() { assertThat(seamlessText.getText()).isEqualTo(context.getResources().getString( com.android.internal.R.string.ext_media_seamless_action)) } + + @Test + fun bindDeviceResumptionPlayer() { + player.attach(holder) + val state = MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(), + emptyList(), PACKAGE, session.getSessionToken(), null, device, true, null, + resumption = true) + player.bind(state) + assertThat(seamlessText.getText()).isEqualTo(DEVICE_NAME) + assertThat(seamless.isEnabled()).isFalse() + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataCombineLatestTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataCombineLatestTest.java index 618ee892b2b04..9fdd9ad744ff4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataCombineLatestTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataCombineLatestTest.java @@ -79,8 +79,8 @@ public class MediaDataCombineLatestTest extends SysuiTestCase { mManager.addListener(mListener); mMediaData = new MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null, - new ArrayList<>(), new ArrayList<>(), PACKAGE, null, null, null, true, null, KEY, - false); + new ArrayList<>(), new ArrayList<>(), PACKAGE, null, null, null, true, null, false, + KEY, false); mDeviceData = new MediaDeviceData(true, null, DEVICE_NAME); } 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 54520be8a03f6..20a6da5488482 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt @@ -1,6 +1,11 @@ package com.android.systemui.media -import android.app.Notification +import android.app.Notification.MediaStyle +import android.app.PendingIntent +import android.media.MediaDescription +import android.media.MediaMetadata +import android.media.session.MediaController +import android.media.session.MediaSession import android.service.notification.StatusBarNotification import android.testing.AndroidTestingRunner import android.testing.TestableLooper.RunWithLooper @@ -8,6 +13,9 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dump.DumpManager +import com.android.systemui.statusbar.SbnBuilder +import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat import org.junit.After import org.junit.Before @@ -18,12 +26,14 @@ import org.mockito.Mock import org.mockito.Mockito import org.mockito.Mockito.mock import org.mockito.Mockito.verify -import org.mockito.junit.MockitoJUnit -import java.util.concurrent.Executor import org.mockito.Mockito.`when` as whenever +import org.mockito.junit.MockitoJUnit private const val KEY = "KEY" private const val PACKAGE_NAME = "com.android.systemui" +private const val APP_NAME = "SystemUI" +private const val SESSION_ARTIST = "artist" +private const val SESSION_TITLE = "title" private fun eq(value: T): T = Mockito.eq(value) ?: value private fun anyObject(): T { @@ -36,33 +46,47 @@ private fun anyObject(): T { class MediaDataManagerTest : SysuiTestCase() { @Mock lateinit var mediaControllerFactory: MediaControllerFactory - @Mock lateinit var backgroundExecutor: Executor - @Mock lateinit var foregroundExecutor: Executor + @Mock lateinit var controller: MediaController + lateinit var session: MediaSession + lateinit var metadataBuilder: MediaMetadata.Builder + lateinit var backgroundExecutor: FakeExecutor + lateinit var foregroundExecutor: FakeExecutor @Mock lateinit var dumpManager: DumpManager @Mock lateinit var broadcastDispatcher: BroadcastDispatcher @Mock lateinit var mediaTimeoutListener: MediaTimeoutListener @Mock lateinit var mediaResumeListener: MediaResumeListener + @Mock lateinit var pendingIntent: PendingIntent @JvmField @Rule val mockito = MockitoJUnit.rule() lateinit var mediaDataManager: MediaDataManager lateinit var mediaNotification: StatusBarNotification @Before fun setup() { + foregroundExecutor = FakeExecutor(FakeSystemClock()) + backgroundExecutor = FakeExecutor(FakeSystemClock()) mediaDataManager = MediaDataManager(context, backgroundExecutor, foregroundExecutor, mediaControllerFactory, broadcastDispatcher, dumpManager, mediaTimeoutListener, mediaResumeListener, useMediaResumption = true, useQsMediaPlayer = true) - val sbn = mock(StatusBarNotification::class.java) - val notification = mock(Notification::class.java) - whenever(notification.hasMediaSession()).thenReturn(true) - whenever(notification.notificationStyle).thenReturn(Notification.MediaStyle::class.java) - whenever(sbn.notification).thenReturn(notification) - whenever(sbn.packageName).thenReturn(PACKAGE_NAME) - mediaNotification = sbn + session = MediaSession(context, "MediaDataManagerTestSession") + mediaNotification = SbnBuilder().run { + setPkg(PACKAGE_NAME) + modifyNotification(context).also { + it.setSmallIcon(android.R.drawable.ic_media_pause) + it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) }) + } + build() + } + metadataBuilder = MediaMetadata.Builder().apply { + putString(MediaMetadata.METADATA_KEY_ARTIST, SESSION_ARTIST) + putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_TITLE) + } + whenever(mediaControllerFactory.create(eq(session.sessionToken))).thenReturn(controller) } @After fun tearDown() { + session.release() mediaDataManager.destroy() } @@ -82,7 +106,7 @@ class MediaDataManagerTest : SysuiTestCase() { @Test fun testLoadsMetadataOnBackground() { mediaDataManager.onNotificationAdded(KEY, mediaNotification) - verify(backgroundExecutor).execute(anyObject()) + assertThat(backgroundExecutor.numPending()).isEqualTo(1) } @Test @@ -123,4 +147,66 @@ class MediaDataManagerTest : SysuiTestCase() { verify(listener).onMediaDataRemoved(eq(KEY)) } -} \ No newline at end of file + + @Test + fun testOnNotificationRemoved_withResumption() { + // GIVEN that the manager has a notification with a resume action + val listener = TestListener() + mediaDataManager.addListener(listener) + whenever(controller.metadata).thenReturn(metadataBuilder.build()) + mediaDataManager.onNotificationAdded(KEY, mediaNotification) + assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) + assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + val data = listener.data!! + assertThat(data.resumption).isFalse() + mediaDataManager.onMediaDataLoaded(KEY, null, data.copy(resumeAction = Runnable {})) + // WHEN the notification is removed + mediaDataManager.onNotificationRemoved(KEY) + // THEN the media data indicates that it is + assertThat(listener.data!!.resumption).isTrue() + } + + @Test + fun testAddResumptionControls() { + val listener = TestListener() + mediaDataManager.addListener(listener) + // WHEN resumption controls are added` + val desc = MediaDescription.Builder().run { + setTitle(SESSION_TITLE) + build() + } + mediaDataManager.addResumptionControls(desc, Runnable {}, session.sessionToken, APP_NAME, + pendingIntent, PACKAGE_NAME) + assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) + assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + // THEN the media data indicates that it is for resumption + val data = listener.data!! + assertThat(data.resumption).isTrue() + assertThat(data.song).isEqualTo(SESSION_TITLE) + assertThat(data.app).isEqualTo(APP_NAME) + assertThat(data.actions).hasSize(1) + } + + /** + * Simple implementation of [MediaDataManager.Listener] for the test. + * + * Giving up on trying to get a mock Listener and ArgumentCaptor to work. + */ + private class TestListener : MediaDataManager.Listener { + var data: MediaData? = null + var key: String? = null + var oldKey: String? = null + + override fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) { + this.key = key + this.oldKey = oldKey + this.data = data + } + + override fun onMediaDataRemoved(key: String) { + this.key = key + oldKey = null + data = null + } + } +}