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:
Beth Thibodeau
2023-01-27 23:39:22 -06:00
parent 121599e660
commit dd6ab19b33
10 changed files with 198 additions and 67 deletions

View File

@@ -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")

View File

@@ -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 */

View File

@@ -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.
*

View File

@@ -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,
)
)
}

View File

@@ -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 =

View File

@@ -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;
}
}

View File

@@ -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)
}

View File

@@ -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);
}

View File

@@ -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)
)
}
}

View File

@@ -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)