diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index 6032da6d0b6b4..9595bc421b5fb 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -361,6 +361,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)