From dd6ab19b33033889f88e0e0b596da2963eeec595 Mon Sep 17 00:00:00 2001 From: Beth Thibodeau Date: Fri, 27 Jan 2023 23:39:22 -0600 Subject: [PATCH] Check for progress info from resume controls Progress information can be specified by apps using extra keys in their MediaDescription. This checks those fields for progress data and sets the seekbar for resume controls. Fixes: 264691253 Test: atest MediaDataManagerTest MediaControlPanelTest Test: manual with test app Change-Id: I52617fe6007c8a60560724bb6672aa8ec8630c88 --- .../src/com/android/systemui/flags/Flags.kt | 3 + .../media/controls/models/player/MediaData.kt | 3 + .../models/player/SeekBarViewModel.kt | 18 ++ .../controls/pipeline/MediaDataManager.kt | 7 + .../media/controls/ui/MediaControlPanel.java | 12 +- .../media/controls/util/MediaDataUtils.java | 35 ++++ .../media/controls/util/MediaFlags.kt | 3 + .../pipeline/MediaDataCombineLatestTest.java | 2 +- .../controls/pipeline/MediaDataManagerTest.kt | 171 +++++++++++------- .../controls/ui/MediaControlPanelTest.kt | 11 ++ 10 files changed, 198 insertions(+), 67 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index 12d36bca9a951..2516a1bb043c6 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -369,6 +369,9 @@ object Flags { @JvmField val MEDIA_RECOMMENDATION_CARD_UPDATE = unreleasedFlag(914, "media_recommendation_card_update") + // TODO(b/267007629): Tracking Bug + val MEDIA_RESUME_PROGRESS = unreleasedFlag(915, "media_resume_progress") + // 1000 - dock val SIMULATE_DOCK_THROUGH_CHARGING = releasedFlag(1000, "simulate_dock_through_charging") diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaData.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaData.kt index be18cbec7163a..b7a2522037afd 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaData.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaData.kt @@ -92,6 +92,9 @@ data class MediaData( /** Whether explicit indicator exists */ val isExplicit: Boolean = false, + + /** Track progress (0 - 1) to display for players where [resumption] is true */ + val resumeProgress: Double? = null, ) { companion object { /** Media is playing on the local device */ diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarViewModel.kt index bba5f350dd162..a057c9f22be37 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarViewModel.kt @@ -237,6 +237,24 @@ constructor( checkIfPollingNeeded() } + /** + * Set the progress to a fixed percentage value that cannot be changed by the user. + * + * @param percent value between 0 and 1 + */ + fun updateStaticProgress(percent: Double) { + val position = (percent * 100).toInt() + _data = + Progress( + enabled = true, + seekAvailable = false, + playing = false, + scrubbing = false, + elapsedTime = position, + duration = 100, + ) + } + /** * Puts the seek bar into a resumption state. * diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt index b11f628623bb6..aba3e98cc7779 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt @@ -67,6 +67,7 @@ import com.android.systemui.media.controls.models.recommendation.SmartspaceMedia import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaDataProvider import com.android.systemui.media.controls.resume.MediaResumeListener import com.android.systemui.media.controls.util.MediaControllerFactory +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.plugins.ActivityStarter @@ -667,6 +668,11 @@ class MediaDataManager( MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT && mediaFlags.isExplicitIndicatorEnabled() + val progress = + if (mediaFlags.isResumeProgressEnabled()) { + MediaDataUtils.getDescriptionProgress(desc.extras) + } else null + val mediaAction = getResumeMediaAction(resumeAction) val lastActive = systemClock.elapsedRealtime() foregroundExecutor.execute { @@ -697,6 +703,7 @@ class MediaDataManager( instanceId = instanceId, appUid = appUid, isExplicit = isExplicit, + resumeProgress = progress, ) ) } 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 9250a580dfa05..d26f239e0f1a1 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 @@ -115,8 +115,6 @@ import com.android.systemui.util.animation.TransitionLayout; import com.android.systemui.util.concurrency.DelayableExecutor; import com.android.systemui.util.time.SystemClock; -import dagger.Lazy; - import java.net.URISyntaxException; import java.util.ArrayList; import java.util.List; @@ -124,6 +122,7 @@ import java.util.concurrent.Executor; import javax.inject.Inject; +import dagger.Lazy; import kotlin.Triple; import kotlin.Unit; @@ -523,8 +522,13 @@ public class MediaControlPanel { } // Seek Bar - final MediaController controller = getController(); - mBackgroundExecutor.execute(() -> mSeekBarViewModel.updateController(controller)); + if (data.getResumption() && data.getResumeProgress() != null) { + double progress = data.getResumeProgress(); + mSeekBarViewModel.updateStaticProgress(progress); + } else { + final MediaController controller = getController(); + mBackgroundExecutor.execute(() -> mSeekBarViewModel.updateController(controller)); + } // Show the broadcast dialog button only when the le audio is enabled. mShowBroadcastDialogButton = diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaDataUtils.java b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaDataUtils.java index bcfceaa3205e3..85282a1d6c12c 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaDataUtils.java +++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaDataUtils.java @@ -19,8 +19,12 @@ package com.android.systemui.media.controls.util; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; +import android.os.Bundle; import android.text.TextUtils; +import androidx.core.math.MathUtils; +import androidx.media.utils.MediaConstants; + /** * Utility class with common methods for media controls */ @@ -50,4 +54,35 @@ public class MediaDataUtils { : unknownName); return applicationName; } + + /** + * Check the bundle for extras indicating the progress percentage + * + * @param extras + * @return the progress value between 0-1 inclusive if prsent, otherwise null + */ + public static Double getDescriptionProgress(Bundle extras) { + if (!extras.containsKey(MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS)) { + return null; + } + + int status = extras.getInt(MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS); + switch (status) { + case MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_NOT_PLAYED: + return 0.0; + case MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_FULLY_PLAYED: + return 1.0; + case MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED: { + if (extras + .containsKey(MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE)) { + double percent = extras + .getDouble(MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE); + return MathUtils.clamp(percent, 0.0, 1.0); + } else { + return 0.5; + } + } + } + return null; + } } 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 81efa3688aab8..a689dc369b50e 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,4 +55,7 @@ class MediaFlags @Inject constructor(private val featureFlags: FeatureFlags) { /** Check whether we show the updated recommendation card. */ fun isRecommendationCardUpdateEnabled() = featureFlags.isEnabled(Flags.MEDIA_RECOMMENDATION_CARD_UPDATE) + + /** Check whether to get progress information for resume players */ + fun isResumeProgressEnabled() = featureFlags.isEnabled(Flags.MEDIA_RESUME_PROGRESS) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatestTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatestTest.java index c0639f34484cd..0a5b124cdb7b3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatestTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatestTest.java @@ -79,7 +79,7 @@ public class MediaDataCombineLatestTest extends SysuiTestCase { USER_ID, true, APP, null, ARTIST, TITLE, null, new ArrayList<>(), new ArrayList<>(), null, PACKAGE, null, null, null, true, null, MediaData.PLAYBACK_LOCAL, false, KEY, false, false, false, 0L, - InstanceId.fakeInstanceId(-1), -1, false); + InstanceId.fakeInstanceId(-1), -1, false, null); mDeviceData = new MediaDeviceData(true, null, DEVICE_NAME, null, false); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt index 1ac66952fd3fe..53cc78f36f15b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt @@ -644,27 +644,8 @@ class MediaDataManagerTest : SysuiTestCase() { build() } val currentTime = clock.elapsedRealtime() - mediaDataManager.addResumptionControls( - USER_ID, - 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 - verify(listener) - .onMediaDataLoaded( - eq(PACKAGE_NAME), - eq(null), - capture(mediaDataCaptor), - eq(true), - eq(0), - eq(false) - ) + addResumeControlAndLoad(desc) + val data = mediaDataCaptor.value assertThat(data.resumption).isTrue() assertThat(data.song).isEqualTo(SESSION_TITLE) @@ -690,27 +671,8 @@ class MediaDataManagerTest : SysuiTestCase() { build() } val currentTime = clock.elapsedRealtime() - mediaDataManager.addResumptionControls( - USER_ID, - 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 - verify(listener) - .onMediaDataLoaded( - eq(PACKAGE_NAME), - eq(null), - capture(mediaDataCaptor), - eq(true), - eq(0), - eq(false) - ) + addResumeControlAndLoad(desc) + val data = mediaDataCaptor.value assertThat(data.resumption).isTrue() assertThat(data.song).isEqualTo(SESSION_TITLE) @@ -722,6 +684,84 @@ class MediaDataManagerTest : SysuiTestCase() { verify(logger).logResumeMediaAdded(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId)) } + @Test + fun testAddResumptionControls_hasPartialProgress() { + whenever(mediaFlags.isResumeProgressEnabled()).thenReturn(true) + + // WHEN resumption controls are added with partial progress + val progress = 0.5 + val extras = + Bundle().apply { + putInt( + MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS, + MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED + ) + putDouble(MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE, progress) + } + val desc = + MediaDescription.Builder().run { + setTitle(SESSION_TITLE) + setExtras(extras) + build() + } + addResumeControlAndLoad(desc) + + val data = mediaDataCaptor.value + assertThat(data.resumption).isTrue() + assertThat(data.resumeProgress).isEqualTo(progress) + } + + @Test + fun testAddResumptionControls_hasNotPlayedProgress() { + whenever(mediaFlags.isResumeProgressEnabled()).thenReturn(true) + + // WHEN resumption controls are added that have not been played + val extras = + Bundle().apply { + putInt( + MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS, + MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_NOT_PLAYED + ) + } + val desc = + MediaDescription.Builder().run { + setTitle(SESSION_TITLE) + setExtras(extras) + build() + } + addResumeControlAndLoad(desc) + + val data = mediaDataCaptor.value + assertThat(data.resumption).isTrue() + assertThat(data.resumeProgress).isEqualTo(0) + } + + @Test + fun testAddResumptionControls_hasFullProgress() { + whenever(mediaFlags.isResumeProgressEnabled()).thenReturn(true) + + // WHEN resumption controls are added with progress info + val extras = + Bundle().apply { + putInt( + MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS, + MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_FULLY_PLAYED + ) + } + val desc = + MediaDescription.Builder().run { + setTitle(SESSION_TITLE) + setExtras(extras) + build() + } + addResumeControlAndLoad(desc) + + // THEN the media data includes the progress + val data = mediaDataCaptor.value + assertThat(data.resumption).isTrue() + assertThat(data.resumeProgress).isEqualTo(1) + } + @Test fun testResumptionDisabled_dismissesResumeControls() { // WHEN there are resume controls and resumption is switched off @@ -730,26 +770,8 @@ class MediaDataManagerTest : SysuiTestCase() { setTitle(SESSION_TITLE) build() } - mediaDataManager.addResumptionControls( - USER_ID, - desc, - Runnable {}, - session.sessionToken, - APP_NAME, - pendingIntent, - PACKAGE_NAME - ) - assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) - assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) - verify(listener) - .onMediaDataLoaded( - eq(PACKAGE_NAME), - eq(null), - capture(mediaDataCaptor), - eq(true), - eq(0), - eq(false) - ) + addResumeControlAndLoad(desc) + val data = mediaDataCaptor.value mediaDataManager.setMediaResumptionEnabled(false) @@ -1690,4 +1712,29 @@ class MediaDataManagerTest : SysuiTestCase() { stateBuilder.setState(PlaybackState.STATE_PAUSED, 0, 1.0f) whenever(controller.playbackState).thenReturn(stateBuilder.build()) } + + /** Helper function to add a resumption control and capture the resulting MediaData */ + private fun addResumeControlAndLoad(desc: MediaDescription) { + mediaDataManager.addResumptionControls( + USER_ID, + desc, + Runnable {}, + session.sessionToken, + APP_NAME, + pendingIntent, + PACKAGE_NAME + ) + assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) + assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + + verify(listener) + .onMediaDataLoaded( + eq(PACKAGE_NAME), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + } } 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 ce22b19b3721a..26b9204f383f0 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 @@ -905,6 +905,17 @@ public class MediaControlPanelTest : SysuiTestCase() { verify(expandedSet).setVisibility(R.id.actionNext, ConstraintSet.VISIBLE) } + @Test + fun bind_resumeState_withProgress() { + val progress = 0.5 + val state = mediaData.copy(resumption = true, resumeProgress = progress) + + player.attachPlayer(viewHolder) + player.bindPlayer(state, PACKAGE) + + verify(seekBarViewModel).updateStaticProgress(progress) + } + @Test fun bindNotificationActions() { val icon = context.getDrawable(android.R.drawable.ic_media_play)