diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 7069e716087de..dd69fa0bebb20 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -2303,6 +2303,7 @@
+
diff --git a/packages/SystemUI/res/drawable/ic_media_connecting_container.xml b/packages/SystemUI/res/drawable/ic_media_connecting_container.xml
new file mode 100644
index 0000000000000..79d2a0601e08f
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_media_connecting_container.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 33ed7b8421db6..b248efe93e98b 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -2199,6 +2199,8 @@
Previous track
Next track
+
+ Connecting
Play
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
index fffb30720af61..05b2c5055e440 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
@@ -660,38 +660,43 @@ public class MediaControlPanel {
final ImageButton button, MediaAction mediaAction, ConstraintSet collapsedSet,
ConstraintSet expandedSet, boolean showInCompact) {
- animHandler.unregisterAll();
if (mediaAction != null) {
- final Drawable icon = mediaAction.getIcon();
- button.setImageDrawable(icon);
- button.setContentDescription(mediaAction.getContentDescription());
- final Drawable bgDrawable = mediaAction.getBackground();
- button.setBackground(bgDrawable);
+ if (animHandler.updateRebindId(mediaAction.getRebindId())) {
+ animHandler.unregisterAll();
- animHandler.tryRegister(icon);
- animHandler.tryRegister(bgDrawable);
+ final Drawable icon = mediaAction.getIcon();
+ button.setImageDrawable(icon);
+ button.setContentDescription(mediaAction.getContentDescription());
+ final Drawable bgDrawable = mediaAction.getBackground();
+ button.setBackground(bgDrawable);
- Runnable action = mediaAction.getAction();
- if (action == null) {
- button.setEnabled(false);
- } else {
- button.setEnabled(true);
- button.setOnClickListener(v -> {
- if (!mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
- mLogger.logTapAction(button.getId(), mUid, mPackageName, mInstanceId);
- logSmartspaceCardReported(SMARTSPACE_CARD_CLICK_EVENT);
- action.run();
+ animHandler.tryRegister(icon);
+ animHandler.tryRegister(bgDrawable);
- if (icon instanceof Animatable) {
- ((Animatable) icon).start();
+ Runnable action = mediaAction.getAction();
+ if (action == null) {
+ button.setEnabled(false);
+ } else {
+ button.setEnabled(true);
+ button.setOnClickListener(v -> {
+ if (!mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
+ mLogger.logTapAction(button.getId(), mUid, mPackageName, mInstanceId);
+ mLogger.logTapAction(button.getId(), mUid, mPackageName, mInstanceId);
+ logSmartspaceCardReported(SMARTSPACE_CARD_CLICK_EVENT);
+ action.run();
+
+ if (icon instanceof Animatable) {
+ ((Animatable) icon).start();
+ }
+ if (bgDrawable instanceof Animatable) {
+ ((Animatable) bgDrawable).start();
+ }
}
- if (bgDrawable instanceof Animatable) {
- ((Animatable) bgDrawable).start();
- }
- }
- });
+ });
+ }
}
} else {
+ animHandler.unregisterAll();
button.setImageDrawable(null);
button.setContentDescription(null);
button.setEnabled(false);
@@ -702,9 +707,29 @@ public class MediaControlPanel {
setVisibleAndAlpha(expandedSet, button.getId(), mediaAction != null);
}
+ // AnimationBindHandler is responsible for tracking the bound animation state and preventing
+ // jank and conflicts due to media notifications arriving at any time during an animation. It
+ // does this in two parts.
+ // - Exit animations fired as a result of user input are tracked. When these are running, any
+ // bind actions are delayed until the animation completes (and then fired in sequence).
+ // - Continuous animations are tracked using their rebind id. Later calls using the same
+ // rebind id will be totally ignored to prevent the continuous animation from restarting.
private static class AnimationBindHandler extends Animatable2.AnimationCallback {
private ArrayList mOnAnimationsComplete = new ArrayList<>();
private ArrayList mRegistrations = new ArrayList<>();
+ private Integer mRebindId = null;
+
+ // This check prevents rebinding to the action button if the identifier has not changed. A
+ // null value is always considered to be changed. This is used to prevent the connecting
+ // animation from rebinding (and restarting) if multiple buffer PlaybackStates are pushed by
+ // an application in a row.
+ public boolean updateRebindId(Integer rebindId) {
+ if (mRebindId == null || rebindId == null || !mRebindId.equals(rebindId)) {
+ mRebindId = rebindId;
+ return true;
+ }
+ return false;
+ }
public void tryRegister(Drawable drawable) {
if (drawable instanceof Animatable2) {
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaData.kt b/packages/SystemUI/src/com/android/systemui/media/MediaData.kt
index a4d2f7bc96c43..bc8cca55154d8 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaData.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaData.kt
@@ -184,7 +184,12 @@ data class MediaAction(
val icon: Drawable?,
val action: Runnable?,
val contentDescription: CharSequence?,
- val background: Drawable?
+ val background: Drawable?,
+
+ // Rebind Id is used to detect identical rebinds and ignore them. It is intended
+ // to prevent continuously looping animations from restarting due to the arrival
+ // of repeated media notifications that are visually identical.
+ val rebindId: Int? = null
)
/** State of the media device. */
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt
index 908aef41034e9..57c93bae3bbf3 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt
@@ -30,6 +30,7 @@ import android.content.IntentFilter
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.ImageDecoder
+import android.graphics.drawable.Animatable
import android.graphics.drawable.Icon
import android.media.MediaDescription
import android.media.MediaMetadata
@@ -57,6 +58,7 @@ import com.android.systemui.dump.DumpManager
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.plugins.BcSmartspaceDataPlugin
import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState
+import com.android.systemui.statusbar.NotificationMediaManager.isConnectingState
import com.android.systemui.statusbar.notification.row.HybridGroupManager
import com.android.systemui.tuner.TunerService
import com.android.systemui.util.Assert
@@ -777,7 +779,20 @@ class MediaDataManager(
val actions = MediaButton()
controller.playbackState?.let { state ->
// First, check for standard actions
- actions.playOrPause = if (isPlayingState(state.state)) {
+ actions.playOrPause = if (isConnectingState(state.state)) {
+ // Spinner needs to be animating to render anything. Start it here.
+ val drawable = context.getDrawable(
+ com.android.internal.R.drawable.progress_small_material)
+ (drawable as Animatable).start()
+ MediaAction(
+ drawable,
+ null, // no action to perform when clicked
+ context.getString(R.string.controls_media_button_connecting),
+ context.getDrawable(R.drawable.ic_media_connecting_container),
+ // Specify a rebind id to prevent the spinner from restarting on later binds.
+ com.android.internal.R.drawable.progress_small_material
+ )
+ } else if (isPlayingState(state.state)) {
getStandardAction(controller, state.actions, PlaybackState.ACTION_PAUSE)
} else {
getStandardAction(controller, state.actions, PlaybackState.ACTION_PLAY)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java
index 7239d0cc361bb..1df40915a52ca 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java
@@ -102,12 +102,14 @@ public class NotificationMediaManager implements Dumpable {
KeyguardStateController.class);
private final KeyguardBypassController mKeyguardBypassController;
private static final HashSet PAUSED_MEDIA_STATES = new HashSet<>();
+ private static final HashSet CONNECTING_MEDIA_STATES = new HashSet<>();
static {
PAUSED_MEDIA_STATES.add(PlaybackState.STATE_NONE);
PAUSED_MEDIA_STATES.add(PlaybackState.STATE_STOPPED);
PAUSED_MEDIA_STATES.add(PlaybackState.STATE_PAUSED);
PAUSED_MEDIA_STATES.add(PlaybackState.STATE_ERROR);
- PAUSED_MEDIA_STATES.add(PlaybackState.STATE_CONNECTING);
+ CONNECTING_MEDIA_STATES.add(PlaybackState.STATE_CONNECTING);
+ CONNECTING_MEDIA_STATES.add(PlaybackState.STATE_BUFFERING);
}
private final NotificationVisibilityProvider mVisibilityProvider;
@@ -363,7 +365,17 @@ public class NotificationMediaManager implements Dumpable {
* @return true if playing
*/
public static boolean isPlayingState(int state) {
- return !PAUSED_MEDIA_STATES.contains(state);
+ return !PAUSED_MEDIA_STATES.contains(state)
+ && !CONNECTING_MEDIA_STATES.contains(state);
+ }
+
+ /**
+ * Check if a state should be considered as connecting
+ * @param state a PlaybackState
+ * @return true if connecting or buffering
+ */
+ public static boolean isConnectingState(int state) {
+ return CONNECTING_MEDIA_STATES.contains(state);
}
public void setUpWithPresenter(NotificationPresenter presenter) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt
index b9ff8775f2b8d..1921cb624fde5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt
@@ -725,6 +725,25 @@ class MediaDataManagerTest : SysuiTestCase() {
assertThat(actions.custom1!!.contentDescription).isEqualTo(customDesc[3])
}
+ @Test
+ fun testPlaybackActions_connecting() {
+ whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true)
+ val stateActions = PlaybackState.ACTION_PLAY
+ val stateBuilder = PlaybackState.Builder()
+ .setState(PlaybackState.STATE_BUFFERING, 0, 10f)
+ .setActions(stateActions)
+ whenever(controller.playbackState).thenReturn(stateBuilder.build())
+
+ addNotificationAndLoad()
+
+ assertThat(mediaDataCaptor.value!!.semanticActions).isNotNull()
+ val actions = mediaDataCaptor.value!!.semanticActions!!
+
+ assertThat(actions.playOrPause).isNotNull()
+ assertThat(actions.playOrPause!!.contentDescription).isEqualTo(
+ context.getString(R.string.controls_media_button_connecting))
+ }
+
@Test
fun testPlaybackActions_reservedSpace() {
val customDesc = arrayOf("custom 1", "custom 2", "custom 3", "custom 4")