Merge "Spinner drawable for casting intermediate state" into tm-dev

This commit is contained in:
Hawkwood Glazier
2022-04-05 16:57:45 +00:00
committed by Android (Google) Code Review
8 changed files with 148 additions and 29 deletions

View File

@@ -2303,6 +2303,7 @@
<java-symbol type="drawable" name="scrubber_control_disabled_holo" />
<java-symbol type="drawable" name="scrubber_control_selector_holo" />
<java-symbol type="drawable" name="scrubber_progress_horizontal_holo_dark" />
<java-symbol type="drawable" name="progress_small_material" />
<java-symbol type="string" name="chooseUsbActivity" />
<java-symbol type="string" name="ext_media_badremoval_notification_message" />
<java-symbol type="string" name="ext_media_badremoval_notification_title" />

View File

@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2022 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="48dp"
android:width="48dp"
android:viewportHeight="48"
android:viewportWidth="48">
<group android:name="_R_G">
<group android:name="_R_G_L_1_G"
android:translateX="24"
android:translateY="24"
android:scaleX="0.5"
android:scaleY="0.5"/>
<group android:name="_R_G_L_0_G"
android:translateX="24"
android:translateY="24"
android:scaleX="0.5"
android:scaleY="0.5">
<path android:name="_R_G_L_0_G_D_0_P_0"
android:fillColor="#ffddb3"
android:fillAlpha="1"
android:fillType="nonZero"
android:pathData=" M48 -16 C48,-16 48,16 48,16 C48,33.67 33.67,48 16,48 C16,48 -16,48 -16,48 C-33.67,48 -48,33.67 -48,16 C-48,16 -48,-16 -48,-16 C-48,-33.67 -33.67,-48 -16,-48 C-16,-48 16,-48 16,-48 C33.67,-48 48,-33.67 48,-16c "/>
</group>
</group>
</vector>

View File

@@ -2199,6 +2199,8 @@
<string name="controls_media_button_prev">Previous track</string>
<!-- Description for button in media controls. Pressing button goes to next track [CHAR_LIMIT=NONE] -->
<string name="controls_media_button_next">Next track</string>
<!-- Description for button in media controls. Used when media is connecting to a remote device (via something like chromecast). Pressing button does nothing [CHAR_LIMIT=NONE] -->
<string name="controls_media_button_connecting">Connecting</string>
<!-- Title for Smartspace recommendation card within media controls. The "Play" means the action to play a media [CHAR_LIMIT=10] -->
<string name="controls_media_smartspace_rec_title">Play</string>

View File

@@ -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<Runnable> mOnAnimationsComplete = new ArrayList<>();
private ArrayList<Animatable2> 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) {

View File

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

View File

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

View File

@@ -102,12 +102,14 @@ public class NotificationMediaManager implements Dumpable {
KeyguardStateController.class);
private final KeyguardBypassController mKeyguardBypassController;
private static final HashSet<Integer> PAUSED_MEDIA_STATES = new HashSet<>();
private static final HashSet<Integer> 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) {

View File

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