Merge "Check for progress info from resume controls" into tm-qpr-dev

This commit is contained in:
Beth Thibodeau
2023-01-31 15:55:57 +00:00
committed by Android (Google) Code Review
10 changed files with 198 additions and 67 deletions

View File

@@ -361,6 +361,9 @@ object Flags {
@JvmField @JvmField
val MEDIA_RECOMMENDATION_CARD_UPDATE = unreleasedFlag(914, "media_recommendation_card_update") 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 // 1000 - dock
val SIMULATE_DOCK_THROUGH_CHARGING = releasedFlag(1000, "simulate_dock_through_charging") val SIMULATE_DOCK_THROUGH_CHARGING = releasedFlag(1000, "simulate_dock_through_charging")

View File

@@ -92,6 +92,9 @@ data class MediaData(
/** Whether explicit indicator exists */ /** Whether explicit indicator exists */
val isExplicit: Boolean = false, val isExplicit: Boolean = false,
/** Track progress (0 - 1) to display for players where [resumption] is true */
val resumeProgress: Double? = null,
) { ) {
companion object { companion object {
/** Media is playing on the local device */ /** Media is playing on the local device */

View File

@@ -237,6 +237,24 @@ constructor(
checkIfPollingNeeded() 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. * 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.models.recommendation.SmartspaceMediaDataProvider
import com.android.systemui.media.controls.resume.MediaResumeListener import com.android.systemui.media.controls.resume.MediaResumeListener
import com.android.systemui.media.controls.util.MediaControllerFactory 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.MediaFlags
import com.android.systemui.media.controls.util.MediaUiEventLogger import com.android.systemui.media.controls.util.MediaUiEventLogger
import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.ActivityStarter
@@ -667,6 +668,11 @@ class MediaDataManager(
MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT && MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT &&
mediaFlags.isExplicitIndicatorEnabled() mediaFlags.isExplicitIndicatorEnabled()
val progress =
if (mediaFlags.isResumeProgressEnabled()) {
MediaDataUtils.getDescriptionProgress(desc.extras)
} else null
val mediaAction = getResumeMediaAction(resumeAction) val mediaAction = getResumeMediaAction(resumeAction)
val lastActive = systemClock.elapsedRealtime() val lastActive = systemClock.elapsedRealtime()
foregroundExecutor.execute { foregroundExecutor.execute {
@@ -697,6 +703,7 @@ class MediaDataManager(
instanceId = instanceId, instanceId = instanceId,
appUid = appUid, appUid = appUid,
isExplicit = isExplicit, 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.concurrency.DelayableExecutor;
import com.android.systemui.util.time.SystemClock; import com.android.systemui.util.time.SystemClock;
import dagger.Lazy;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -124,6 +122,7 @@ import java.util.concurrent.Executor;
import javax.inject.Inject; import javax.inject.Inject;
import dagger.Lazy;
import kotlin.Triple; import kotlin.Triple;
import kotlin.Unit; import kotlin.Unit;
@@ -523,8 +522,13 @@ public class MediaControlPanel {
} }
// Seek Bar // Seek Bar
final MediaController controller = getController(); if (data.getResumption() && data.getResumeProgress() != null) {
mBackgroundExecutor.execute(() -> mSeekBarViewModel.updateController(controller)); 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. // Show the broadcast dialog button only when the le audio is enabled.
mShowBroadcastDialogButton = mShowBroadcastDialogButton =

View File

@@ -19,8 +19,12 @@ package com.android.systemui.media.controls.util;
import android.content.Context; import android.content.Context;
import android.content.pm.ApplicationInfo; import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.os.Bundle;
import android.text.TextUtils; import android.text.TextUtils;
import androidx.core.math.MathUtils;
import androidx.media.utils.MediaConstants;
/** /**
* Utility class with common methods for media controls * Utility class with common methods for media controls
*/ */
@@ -50,4 +54,35 @@ public class MediaDataUtils {
: unknownName); : unknownName);
return applicationName; 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. */ /** Check whether we show the updated recommendation card. */
fun isRecommendationCardUpdateEnabled() = fun isRecommendationCardUpdateEnabled() =
featureFlags.isEnabled(Flags.MEDIA_RECOMMENDATION_CARD_UPDATE) 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, USER_ID, true, APP, null, ARTIST, TITLE, null,
new ArrayList<>(), new ArrayList<>(), null, PACKAGE, null, null, null, true, null, new ArrayList<>(), new ArrayList<>(), null, PACKAGE, null, null, null, true, null,
MediaData.PLAYBACK_LOCAL, false, KEY, false, false, false, 0L, 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); mDeviceData = new MediaDeviceData(true, null, DEVICE_NAME, null, false);
} }

View File

@@ -644,27 +644,8 @@ class MediaDataManagerTest : SysuiTestCase() {
build() build()
} }
val currentTime = clock.elapsedRealtime() val currentTime = clock.elapsedRealtime()
mediaDataManager.addResumptionControls( addResumeControlAndLoad(desc)
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)
)
val data = mediaDataCaptor.value val data = mediaDataCaptor.value
assertThat(data.resumption).isTrue() assertThat(data.resumption).isTrue()
assertThat(data.song).isEqualTo(SESSION_TITLE) assertThat(data.song).isEqualTo(SESSION_TITLE)
@@ -690,27 +671,8 @@ class MediaDataManagerTest : SysuiTestCase() {
build() build()
} }
val currentTime = clock.elapsedRealtime() val currentTime = clock.elapsedRealtime()
mediaDataManager.addResumptionControls( addResumeControlAndLoad(desc)
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)
)
val data = mediaDataCaptor.value val data = mediaDataCaptor.value
assertThat(data.resumption).isTrue() assertThat(data.resumption).isTrue()
assertThat(data.song).isEqualTo(SESSION_TITLE) assertThat(data.song).isEqualTo(SESSION_TITLE)
@@ -722,6 +684,84 @@ class MediaDataManagerTest : SysuiTestCase() {
verify(logger).logResumeMediaAdded(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId)) 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 @Test
fun testResumptionDisabled_dismissesResumeControls() { fun testResumptionDisabled_dismissesResumeControls() {
// WHEN there are resume controls and resumption is switched off // WHEN there are resume controls and resumption is switched off
@@ -730,26 +770,8 @@ class MediaDataManagerTest : SysuiTestCase() {
setTitle(SESSION_TITLE) setTitle(SESSION_TITLE)
build() build()
} }
mediaDataManager.addResumptionControls( addResumeControlAndLoad(desc)
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)
)
val data = mediaDataCaptor.value val data = mediaDataCaptor.value
mediaDataManager.setMediaResumptionEnabled(false) mediaDataManager.setMediaResumptionEnabled(false)
@@ -1690,4 +1712,29 @@ class MediaDataManagerTest : SysuiTestCase() {
stateBuilder.setState(PlaybackState.STATE_PAUSED, 0, 1.0f) stateBuilder.setState(PlaybackState.STATE_PAUSED, 0, 1.0f)
whenever(controller.playbackState).thenReturn(stateBuilder.build()) 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) 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 @Test
fun bindNotificationActions() { fun bindNotificationActions() {
val icon = context.getDrawable(android.R.drawable.ic_media_play) val icon = context.getDrawable(android.R.drawable.ic_media_play)