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
This commit is contained in:
@@ -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")
|
||||
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user