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