Media views now dynamically transition between media hosts
Let there be hosts and transitions.
This also enables us to build again after the rebase
Bug: 154137987
Test: atest SystemUITests
Change-Id: I8aaed1718b35be46abae0b0255793f37b8a1fbd2
diff --git a/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt b/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt
index ae7860e..b87a1db 100644
--- a/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt
@@ -39,7 +39,7 @@
fun attach(mediaView: MediaHeaderView) {
view = mediaView
mediaHost.visibleChangedListener = ::updateVisibility
- mediaHost.isExpanded = false
+ mediaHost.expansion = 0.0f
mediaHost.showsOnlyActiveMedia = true
mediaHost.init(MediaHierarchyManager.LOCATION_LOCKSCREEN)
mediaView.setMediaHost(mediaHost.hostView)
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
index 0297c90..673fafa 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
@@ -18,32 +18,23 @@
import static com.android.systemui.util.SysuiLifecycle.viewAttachLifecycle;
-import android.annotation.LayoutRes;
import android.app.PendingIntent;
import android.content.ComponentName;
-import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.res.ColorStateList;
-import android.graphics.Bitmap;
-import android.graphics.ImageDecoder;
-import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.GradientDrawable;
import android.graphics.drawable.RippleDrawable;
-import android.media.MediaDescription;
import android.media.MediaMetadata;
-import android.media.ThumbnailUtils;
import android.media.session.MediaController;
import android.media.session.MediaController.PlaybackInfo;
import android.media.session.MediaSession;
import android.media.session.PlaybackState;
-import android.net.Uri;
import android.service.media.MediaBrowserService;
-import android.text.TextUtils;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
@@ -56,10 +47,11 @@
import android.widget.TextView;
import androidx.annotation.Nullable;
+import androidx.constraintlayout.motion.widget.Key;
+import androidx.constraintlayout.motion.widget.KeyAttributes;
+import androidx.constraintlayout.motion.widget.KeyFrames;
import androidx.constraintlayout.motion.widget.MotionLayout;
import androidx.constraintlayout.widget.ConstraintSet;
-import androidx.core.graphics.drawable.RoundedBitmapDrawable;
-import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
import com.android.settingslib.media.LocalMediaManager;
import com.android.settingslib.media.MediaDevice;
@@ -73,7 +65,7 @@
import org.jetbrains.annotations.NotNull;
-import java.io.IOException;
+import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
@@ -100,7 +92,8 @@
private final ActivityStarter mActivityStarter;
private Context mContext;
- protected MotionLayout mMediaNotifView;
+ private MotionLayout mMediaNotifView;
+ private final View mBackground;
private View mSeamless;
private MediaSession.Token mToken;
private MediaController mController;
@@ -109,6 +102,7 @@
private MediaDevice mDevice;
protected ComponentName mServiceComponent;
private boolean mIsRegistered = false;
+ private final List<KeyFrames> mKeyFrames;
private String mKey;
public static final String MEDIA_PREFERENCES = "media_control_prefs";
@@ -184,8 +178,6 @@
* @param context
* @param parent
* @param routeManager Manager used to listen for device change events.
- * @param layoutId layout resource to use for this control panel
- * @param actionIds resource IDs for action buttons in the layout
* @param foregroundExecutor foreground executor
* @param backgroundExecutor background executor, used for processing artwork
* @param activityStarter activity starter
@@ -196,6 +188,8 @@
mContext = context;
LayoutInflater inflater = LayoutInflater.from(mContext);
mMediaNotifView = (MotionLayout) inflater.inflate(R.layout.qs_media_panel, parent, false);
+ mBackground = mMediaNotifView.findViewById(R.id.media_background);
+ mKeyFrames = mMediaNotifView.getDefinedTransitions().get(0).getKeyFrameList();
// TODO(b/150854549): removeOnAttachStateChangeListener when this doesn't inflate views
// mStateListener shouldn't need to be unregistered since this object shares the same
// lifecycle with the inflated view. It would be better, however, if this controller used an
@@ -250,7 +244,7 @@
* Bind this view based on the data given
*/
public void bind(@NotNull MediaData data) {
- mToken = data.getToken();
+ MediaSession.Token token = data.getToken();
mForegroundColor = data.getForegroundColor();
mBackgroundColor = data.getBackgroundColor();
if (mToken == null || !mToken.equals(token)) {
@@ -265,7 +259,6 @@
}
mController = new MediaController(mContext, mToken);
- mKey = key;
// Try to find a browser service component for this app
// TODO also check for a media button receiver intended for restarting (b/154127084)
@@ -291,9 +284,8 @@
mController.registerCallback(mSessionCallback);
- albumView.setImageDrawable(data.getArtwork());
-
- mMediaNotifView.setBackgroundTintList(ColorStateList.valueOf(mBackgroundColor));
+ mMediaNotifView.requireViewById(R.id.media_background).setBackgroundTintList(
+ ColorStateList.valueOf(mBackgroundColor));
// Click action
PendingIntent clickIntent = data.getClickIntent();
@@ -303,6 +295,9 @@
});
}
+ ImageView albumView = mMediaNotifView.findViewById(R.id.album_art);
+ albumView.setImageDrawable(data.getArtwork());
+
// App icon
ImageView appIcon = mMediaNotifView.requireViewById(R.id.icon);
// TODO: look at iconDrawable
@@ -310,14 +305,21 @@
iconDrawable.setTint(mForegroundColor);
appIcon.setImageDrawable(iconDrawable);
+ // Song name
TextView titleText = mMediaNotifView.requireViewById(R.id.header_title);
titleText.setText(data.getSong());
+ titleText.setTextColor(data.getForegroundColor());
+
+ // App title
TextView appName = mMediaNotifView.requireViewById(R.id.app_name);
appName.setText(data.getApp());
appName.setTextColor(mForegroundColor);
+
+ // Artist name
TextView artistText = mMediaNotifView.requireViewById(R.id.header_artist);
artistText.setText(data.getArtist());
artistText.setTextColor(mForegroundColor);
+
// Transfer chip
mSeamless = mMediaNotifView.findViewById(R.id.media_seamless);
if (mSeamless != null) {
@@ -352,15 +354,16 @@
int i = 0;
List<MediaAction> actionIcons = data.getActions();
for (; i < actionIcons.size() && i < ACTION_IDS.length; i++) {
- final ImageButton button = mMediaNotifView.findViewById(ACTION_IDS[i]);
+ int actionId = ACTION_IDS[i];
+ final ImageButton button = mMediaNotifView.findViewById(actionId);
MediaAction mediaAction = actionIcons.get(i);
button.setImageDrawable(mediaAction.getDrawable());
button.setContentDescription(mediaAction.getContentDescription());
button.setImageTintList(ColorStateList.valueOf(mForegroundColor));
PendingIntent actionIntent = mediaAction.getIntent();
- if (mMediaNotifView.getBackground() instanceof IlluminationDrawable) {
- ((IlluminationDrawable) mMediaNotifView.getBackground())
+ if (mBackground.getBackground() instanceof IlluminationDrawable) {
+ ((IlluminationDrawable) mBackground.getBackground())
.setupTouch(button, mMediaNotifView);
}
@@ -374,9 +377,10 @@
}
});
boolean visibleInCompat = actionsWhenCollapsed.contains(i);
- collapsedSet.setVisibility(ACTION_IDS[i],
+ updateKeyFrameVisibility(actionId, visibleInCompat);
+ collapsedSet.setVisibility(actionId,
visibleInCompat ? ConstraintSet.VISIBLE : ConstraintSet.GONE);
- expandedSet.setVisibility(ACTION_IDS[i], ConstraintSet.VISIBLE);
+ expandedSet.setVisibility(actionId, ConstraintSet.VISIBLE);
}
// Hide any unused buttons
@@ -394,41 +398,26 @@
// TODO: b/156036025 bring back media guts
makeActive();
+ }
- // App title (not in mini player)
- TextView appName = mMediaNotifView.findViewById(R.id.app_name);
- if (appName != null) {
- appName.setText(appNameString);
- appName.setTextColor(mForegroundColor);
- }
-
- // Can be null!
- MediaMetadata mediaMetadata = mController.getMetadata();
-
- ImageView albumView = mMediaNotifView.findViewById(R.id.album_art);
- if (albumView != null) {
- // Resize art in a background thread
- mBackgroundExecutor.execute(() -> processAlbumArt(mediaMetadata, largeIcon, albumView));
- }
-
- // Song name
- TextView titleText = mMediaNotifView.findViewById(R.id.header_title);
- String songName = "";
- if (mediaMetadata != null) {
- songName = mediaMetadata.getString(MediaMetadata.METADATA_KEY_TITLE);
- }
- titleText.setText(songName);
- titleText.setTextColor(mForegroundColor);
-
- // Artist name (not in mini player)
- TextView artistText = mMediaNotifView.findViewById(R.id.header_artist);
- if (artistText != null) {
- String artistName = "";
- if (mediaMetadata != null) {
- artistName = mediaMetadata.getString(MediaMetadata.METADATA_KEY_ARTIST);
+ /**
+ * Updates the keyframe visibility such that only views that are not visible actually go
+ * through a transition and fade in.
+ *
+ * @param actionId the id to change
+ * @param visible is the view visible
+ */
+ private void updateKeyFrameVisibility(int actionId, boolean visible) {
+ for (int i = 0; i < mKeyFrames.size(); i++) {
+ KeyFrames keyframe = mKeyFrames.get(i);
+ ArrayList<Key> viewKeyFrames = keyframe.getKeyFramesForView(actionId);
+ for (int j = 0; j < viewKeyFrames.size(); j++) {
+ Key key = viewKeyFrames.get(j);
+ if (key instanceof KeyAttributes) {
+ KeyAttributes attributes = (KeyAttributes) key;
+ attributes.setValue("alpha", visible ? 1.0f : 0.0f);
+ }
}
- artistText.setText(artistName);
- artistText.setTextColor(mForegroundColor);
}
}
@@ -502,120 +491,6 @@
}
/**
- * Process album art for layout
- * @param description media description
- * @param albumView view to hold the album art
- */
- protected void processAlbumArt(MediaDescription description, ImageView albumView) {
- Bitmap albumArt = null;
-
- // First try loading from URI
- albumArt = loadBitmapFromUri(description.getIconUri());
-
- // Then check bitmap
- if (albumArt == null) {
- albumArt = description.getIconBitmap();
- }
-
- processAlbumArtInternal(albumArt, albumView);
- }
-
- /**
- * Process album art for layout
- * @param metadata media metadata
- * @param largeIcon from notification, checked as a fallback if metadata does not have art
- * @param albumView view to hold the album art
- */
- private void processAlbumArt(MediaMetadata metadata, Icon largeIcon, ImageView albumView) {
- Bitmap albumArt = null;
-
- if (metadata != null) {
- // First look in URI fields
- for (String field : ART_URIS) {
- String uriString = metadata.getString(field);
- if (!TextUtils.isEmpty(uriString)) {
- albumArt = loadBitmapFromUri(Uri.parse(uriString));
- if (albumArt != null) {
- Log.d(TAG, "loaded art from " + field);
- break;
- }
- }
- }
-
- // Then check bitmap field
- if (albumArt == null) {
- albumArt = metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART);
- }
- }
-
- // Finally try the notification's largeIcon
- if (albumArt == null && largeIcon != null) {
- albumArt = largeIcon.getBitmap();
- }
-
- processAlbumArtInternal(albumArt, albumView);
- }
-
- /**
- * Load a bitmap from a URI
- * @param uri
- * @return bitmap, or null if couldn't be loaded
- */
- private Bitmap loadBitmapFromUri(Uri uri) {
- // ImageDecoder requires a scheme of the following types
- if (uri.getScheme() == null) {
- return null;
- }
-
- if (!uri.getScheme().equals(ContentResolver.SCHEME_CONTENT)
- && !uri.getScheme().equals(ContentResolver.SCHEME_ANDROID_RESOURCE)
- && !uri.getScheme().equals(ContentResolver.SCHEME_FILE)) {
- return null;
- }
-
- ImageDecoder.Source source = ImageDecoder.createSource(mContext.getContentResolver(), uri);
- try {
- return ImageDecoder.decodeBitmap(source);
- } catch (IOException e) {
- e.printStackTrace();
- return null;
- }
- }
-
- /**
- * Resize and crop the image if provided and update the control view
- * @param albumArt Bitmap of art to display, or null to hide view
- * @param albumView View that will hold the art
- */
- private void processAlbumArtInternal(@Nullable Bitmap albumArt, ImageView albumView) {
- // Resize
- RoundedBitmapDrawable roundedDrawable = null;
- if (albumArt != null) {
- float radius = mContext.getResources().getDimension(R.dimen.qs_media_corner_radius);
- Bitmap original = albumArt.copy(Bitmap.Config.ARGB_8888, true);
- int albumSize = (int) mContext.getResources().getDimension(
- R.dimen.qs_media_album_size);
- Bitmap scaled = ThumbnailUtils.extractThumbnail(original, albumSize, albumSize);
- roundedDrawable = RoundedBitmapDrawableFactory.create(mContext.getResources(), scaled);
- roundedDrawable.setCornerRadius(radius);
- } else {
- Log.e(TAG, "No album art available");
- }
-
- // Now that it's resized, update the UI
- final RoundedBitmapDrawable result = roundedDrawable;
- mForegroundExecutor.execute(() -> {
- if (result != null) {
- albumView.setImageDrawable(result);
- albumView.setVisibility(View.VISIBLE);
- } else {
- albumView.setImageDrawable(null);
- albumView.setVisibility(View.GONE);
- }
- });
- }
-
- /**
* Update the current device information
* @param device device information to display
*/
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt
index c9630f4..2e41ffb 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt
@@ -16,22 +16,32 @@
package com.android.systemui.media
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.ValueAnimator
import android.annotation.IntDef
import android.content.Context
import android.view.LayoutInflater
+import android.view.View
import android.view.ViewGroup
+import android.view.ViewGroupOverlay
import android.widget.LinearLayout
import com.android.settingslib.bluetooth.LocalBluetoothManager
import com.android.settingslib.media.InfoMediaManager
import com.android.settingslib.media.LocalMediaManager
+import com.android.systemui.Interpolators
import com.android.systemui.R
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.statusbar.StatusBarState
+import com.android.systemui.statusbar.SysuiStatusBarStateController
import com.android.systemui.statusbar.notification.VisualStabilityManager
+import com.android.systemui.statusbar.notification.stack.StackStateAnimator
import com.android.systemui.statusbar.phone.KeyguardBypassController
+import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.util.animation.UniqueObjectHost
import com.android.systemui.util.concurrency.DelayableExecutor
import java.util.concurrent.Executor
@@ -49,15 +59,46 @@
@Background private val backgroundExecutor: DelayableExecutor,
private val localBluetoothManager: LocalBluetoothManager?,
private val visualStabilityManager: VisualStabilityManager,
- private val statusBarStateController: StatusBarStateController,
+ private val statusBarStateController: SysuiStatusBarStateController,
+ private val keyguardStateController: KeyguardStateController,
private val bypassController: KeyguardBypassController,
+ private val activityStarter: ActivityStarter,
mediaManager: MediaDataManager
) {
+ private var rootOverlay: ViewGroupOverlay? = null
+ private lateinit var currentState: MediaState
+ private var animationStartState: MediaState? = null
+ private var animator = ValueAnimator.ofFloat(0.0f, 1.0f).apply {
+ interpolator = Interpolators.FAST_OUT_SLOW_IN
+ addUpdateListener {
+ updateTargetState()
+ applyState(animationStartState!!.interpolate(targetState!!, animatedFraction))
+ }
+ addListener(object : AnimatorListenerAdapter() {
+ private var cancelled: Boolean = false
+
+ override fun onAnimationCancel(animation: Animator?) {
+ cancelled = true
+ }
+ override fun onAnimationEnd(animation: Animator?) {
+ if (!cancelled) {
+ applyTargetStateIfNotAnimating()
+ }
+ }
+
+ override fun onAnimationStart(animation: Animator?) {
+ cancelled = false
+ }
+ })
+ }
+ private var targetState: MediaState? = null
private val mediaCarousel: ViewGroup
private val mediaContent: ViewGroup
private val mediaPlayers: MutableMap<String, MediaControlPanel> = mutableMapOf()
private val mediaHosts = arrayOfNulls<MediaHost>(LOCATION_LOCKSCREEN + 1)
private val visualStabilityCallback = ::reorderAllPlayers
+ private var previousLocation = -1
+ private var desiredLocation = -1
private var currentAttachmentLocation = -1
var shouldListen = true
@@ -68,6 +109,18 @@
}
}
+ var qsExpansion: Float = 0.0f
+ set(value) {
+ if (field != value) {
+ field = value
+ updateDesiredLocation()
+ if (getQSTransformationProgress() >= 0) {
+ updateTargetState()
+ applyTargetStateIfNotAnimating()
+ }
+ }
+ }
+
init {
mediaCarousel = inflateMediaCarousel()
mediaContent = mediaCarousel.requireViewById(R.id.media_carousel)
@@ -83,6 +136,11 @@
}
}
})
+ statusBarStateController.addCallback(object : StatusBarStateController.StateListener {
+ override fun onStateChanged(newState: Int) {
+ updateDesiredLocation()
+ }
+ })
}
private fun inflateMediaCarousel(): ViewGroup {
@@ -112,7 +170,7 @@
imm, data.packageName)
existingPlayer = MediaControlPanel(context, mediaContent, routeManager,
- foregroundExecutor, backgroundExecutor)
+ foregroundExecutor, backgroundExecutor, activityStarter)
mediaPlayers[key] = existingPlayer
val lp = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT)
@@ -158,56 +216,209 @@
* @return the hostView associated with this location
*/
fun register(mediaObject: MediaHost) : ViewGroup {
- val viewHost = UniqueObjectHost(context)
+ val viewHost = createUniqueObjectHost()
mediaObject.hostView = viewHost;
mediaHosts[mediaObject.location] = mediaObject
- if (mediaObject.location == currentAttachmentLocation) {
+ if (mediaObject.location == desiredLocation) {
// In case we are overriding a view that is already visible, make sure we attach it
// to this new host view in the below call
+ desiredLocation = -1
+ }
+ if (mediaObject.location == currentAttachmentLocation) {
currentAttachmentLocation = -1
}
- updateAttachmentLocation()
+ updateDesiredLocation()
return viewHost
}
- private fun updateAttachmentLocation() {
- var desiredLocation = calculateLocation()
- if (desiredLocation != currentAttachmentLocation) {
- val host = mediaHosts[desiredLocation]
- host?.apply {
- // Remove the carousel from the old host
- (mediaCarousel.parent as ViewGroup?)?.removeView(mediaCarousel)
-
- // Add it to the new one
- host.hostView.addView(mediaCarousel)
-
- // Let's perform a transition
- var previousHost = if (currentAttachmentLocation < 0) {
- null
- } else {
- mediaHosts[currentAttachmentLocation]
+ private fun createUniqueObjectHost(): UniqueObjectHost {
+ val viewHost = UniqueObjectHost(context)
+ viewHost.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
+ override fun onViewAttachedToWindow(p0: View?) {
+ if (rootOverlay == null) {
+ rootOverlay = (viewHost.viewRootImpl.view.overlay as ViewGroupOverlay)
}
- performTransition(previousHost, host)
- currentAttachmentLocation = desiredLocation
+ viewHost.removeOnAttachStateChangeListener(this)
+ }
+
+ override fun onViewDetachedFromWindow(p0: View?) {
+ }
+ })
+ return viewHost
+ }
+
+ private fun updateDesiredLocation() {
+ val desiredLocation = calculateLocation()
+ if (desiredLocation != this.desiredLocation) {
+ if (this.desiredLocation >= 0) {
+ previousLocation = this.desiredLocation
+ }
+ val isNewView = this.desiredLocation == -1
+ this.desiredLocation = desiredLocation
+ // Let's perform a transition
+ performTransition(applyImmediately = isNewView)
+ }
+ }
+
+ private fun performTransition(applyImmediately: Boolean) {
+ if (previousLocation < 0 || applyImmediately) {
+ cancelAnimationAndApplyDesiredState()
+ return
+ }
+ val currentHost = getHost(desiredLocation)
+ val previousHost = getHost(previousLocation)
+ if (currentHost == null || previousHost == null) {
+ cancelAnimationAndApplyDesiredState()
+ return;
+ }
+ updateTargetState()
+ if (isCurrentlyInGuidedTransformation()) {
+ applyTargetStateIfNotAnimating()
+ } else if (shouldAnimateTransition(currentHost, previousHost)) {
+ animator.cancel()
+ // Let's animate to the new position, starting from the current position
+ animationStartState = currentState.copy()
+ adjustAnimatorForTransition(previousLocation, desiredLocation)
+ animator.start()
+ } else {
+ cancelAnimationAndApplyDesiredState()
+ }
+ }
+
+ private fun shouldAnimateTransition(currentHost: MediaHost, previousHost: MediaHost): Boolean {
+ if (currentHost.location == LOCATION_QQS
+ && previousHost.location == LOCATION_LOCKSCREEN
+ && (statusBarStateController.leaveOpenOnKeyguardHide()
+ || statusBarStateController.state == StatusBarState.SHADE_LOCKED)) {
+ // Usually listening to the isShown is enough to determine this, but there is some
+ // non-trivial reattaching logic happening that will make the view not-shown earlier
+ return true
+ }
+ return mediaCarousel.isShown || animator.isRunning
+ }
+
+ private fun adjustAnimatorForTransition(previousLocation: Int, desiredLocation: Int) {
+ var animDuration = 200L
+ var delay = 0L
+ if (previousLocation == LOCATION_LOCKSCREEN && desiredLocation == LOCATION_QQS) {
+ // Going to the full shade, let's adjust the animation duration
+ if (statusBarStateController.state == StatusBarState.SHADE
+ && keyguardStateController.isKeyguardFadingAway) {
+ delay = keyguardStateController.keyguardFadingAwayDelay
+ }
+ animDuration = StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE.toLong()
+ } else if (previousLocation == LOCATION_QQS && desiredLocation == LOCATION_LOCKSCREEN) {
+ animDuration = StackStateAnimator.ANIMATION_DURATION_APPEAR_DISAPPEAR.toLong()
+ }
+ animator.apply {
+ duration = animDuration
+ startDelay = delay
+ }
+
+ }
+
+ private fun applyTargetStateIfNotAnimating() {
+ if (!animator.isRunning) {
+ // Let's immediately apply the target state (which is interpolated) if there is
+ // no animation running. Otherwise the animation update will already update
+ // the location
+ applyState(targetState!!)
+ }
+ }
+
+ private fun updateTargetState() {
+ if (isCurrentlyInGuidedTransformation()) {
+ val progress = getTransformationProgress()
+ val currentHost = getHost(desiredLocation)!!
+ val previousHost = getHost(previousLocation)!!
+ val newState = currentHost.currentState
+ val previousState = previousHost.currentState
+ targetState = previousState.interpolate(newState, progress)
+ } else {
+ targetState = getHost(desiredLocation)?.currentState
+ }
+ }
+
+ /**
+ * @return true if this transformation is guided by an external progress like a finger
+ */
+ private fun isCurrentlyInGuidedTransformation() : Boolean {
+ return getTransformationProgress() >= 0
+ }
+
+ /**
+ * @return the current transformation progress if we're in a guided transformation and -1
+ * otherwise
+ */
+ private fun getTransformationProgress(): Float {
+ val progress = getQSTransformationProgress()
+ if (progress >= 0) {
+ return progress
+ }
+ return -1.0f
+ }
+
+ private fun getQSTransformationProgress(): Float {
+ val currentHost = getHost(desiredLocation)
+ val previousHost = getHost(previousLocation)
+ if (currentHost?.location == LOCATION_QS) {
+ if (previousHost?.location == LOCATION_QQS) {
+ return qsExpansion
+ }
+ }
+ return -1.0f
+ }
+
+ private fun getHost(@MediaLocation location: Int): MediaHost? {
+ if (location < 0) {
+ return null
+ }
+ return mediaHosts[location]
+ }
+
+ private fun cancelAnimationAndApplyDesiredState() {
+ animator.cancel()
+ getHost(desiredLocation)?.let {
+ applyState(it.currentState)
+ }
+ }
+
+ private fun applyState(state: MediaState) {
+ updateHostAttachment()
+ for (mediaPlayer in mediaPlayers.values) {
+ val view = mediaPlayer.view
+ view.progress = state.expansion
+ }
+ val boundsOnScreen = state.boundsOnScreen
+ if (currentAttachmentLocation == IN_OVERLAY) {
+ mediaCarousel.setLeftTopRightBottom(boundsOnScreen.left, boundsOnScreen.top,
+ boundsOnScreen.right, boundsOnScreen.bottom)
+ }
+ currentState = state.copy()
+ }
+
+ private fun updateHostAttachment() {
+ val inOverlay = isTransitionRunning() && rootOverlay != null
+ val newLocation = if (inOverlay) IN_OVERLAY else desiredLocation
+ if (currentAttachmentLocation != newLocation) {
+ currentAttachmentLocation = newLocation
+
+ // Remove the carousel from the old host
+ (mediaCarousel.parent as ViewGroup?)?.removeView(mediaCarousel)
+
+ // Add it to the new one
+ val targetHost = getHost(desiredLocation)!!.hostView
+ if (inOverlay) {
+ rootOverlay!!.add(mediaCarousel)
+ } else {
+ targetHost.addView(mediaCarousel)
}
}
}
- private fun performTransition(previousHost: MediaHost?, newHost: MediaHost) {
- if (previousHost == null) {
- applyObjectStateImmediately(newHost)
- return
- }
- // TODO: actually transition!
- applyObjectStateImmediately(newHost)
- }
-
- private fun applyObjectStateImmediately(newObject: MediaHost) {
- val expansion = if (newObject.isExpanded) 1.0f else 0.0f;
- for (mediaPlayer in mediaPlayers.values) {
- val view = mediaPlayer.view
- view.progress = expansion
- }
+ private fun isTransitionRunning(): Boolean {
+ return isCurrentlyInGuidedTransformation() && getTransformationProgress() != 1.0f
+ || animator.isRunning
}
@MediaLocation
@@ -216,7 +427,8 @@
&& (statusBarStateController.state == StatusBarState.KEYGUARD
|| statusBarStateController.state == StatusBarState.FULLSCREEN_USER_SWITCHER))
return when {
- qsExpansion > 0.0f -> LOCATION_QS
+ qsExpansion > 0.0f && !onLockscreen -> LOCATION_QS
+ qsExpansion > 0.4f && onLockscreen -> LOCATION_QS
onLockscreen -> LOCATION_LOCKSCREEN
else -> LOCATION_QQS
}
@@ -225,12 +437,6 @@
/**
* The expansion of quick settings
*/
- var qsExpansion: Float = 0.0f
- set(value) {
- field = value
- updateAttachmentLocation()
- }
-
@IntDef(prefix = ["LOCATION_"], value = [LOCATION_QS, LOCATION_QQS, LOCATION_LOCKSCREEN])
@Retention(AnnotationRetention.SOURCE)
annotation class MediaLocation
@@ -250,5 +456,10 @@
* Attached on the lock screen
*/
const val LOCATION_LOCKSCREEN = 2
+
+ /**
+ * Attached at the root of the hierarchy in an overlay
+ */
+ const val IN_OVERLAY = -1000
}
}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt
index 7df61f20..42bf0e8 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt
@@ -1,28 +1,51 @@
package com.android.systemui.media
+import android.graphics.Rect
+import android.util.MathUtils
import android.view.View
import android.view.View.OnAttachStateChangeListener
import android.view.ViewGroup
-import com.android.settingslib.bluetooth.LocalBluetoothManager
-import com.android.settingslib.media.InfoMediaManager
-import com.android.settingslib.media.LocalMediaManager
import com.android.systemui.media.MediaHierarchyManager.MediaLocation
import javax.inject.Inject
-import javax.inject.Singleton
class MediaHost @Inject constructor(
+ private val state: MediaHostState,
private val mediaHierarchyManager: MediaHierarchyManager,
private val mediaDataManager: MediaDataManager
-) {
+) : MediaState by state {
+ lateinit var hostView: ViewGroup
var location: Int = -1
private set
- lateinit var hostView: ViewGroup
- var isExpanded: Boolean = false
- var showsOnlyActiveMedia: Boolean = false
var visibleChangedListener: ((Boolean) -> Unit)? = null
var visible: Boolean = false
private set
+ private val tmpLocationOnScreen: IntArray = intArrayOf(0, 0)
+
+ /**
+ * Get the current Media state. This also updates the location on screen
+ */
+ val currentState : MediaState
+ get () {
+ hostView.getLocationOnScreen(tmpLocationOnScreen)
+ var left = tmpLocationOnScreen[0] + hostView.paddingLeft
+ var top = tmpLocationOnScreen[1] + hostView.paddingTop
+ var right = tmpLocationOnScreen[0] + hostView.width - hostView.paddingRight
+ var bottom = tmpLocationOnScreen[1] + hostView.height - hostView.paddingBottom
+ // Handle cases when the width or height is 0 but it has padding. In those cases
+ // the above could return negative widths, which is wrong
+ if (right < left) {
+ left = 0
+ right = 0;
+ }
+ if (bottom < top) {
+ bottom = 0
+ top = 0;
+ }
+ state.boundsOnScreen.set(left, top, right, bottom)
+ return state
+ }
+
private val listener = object : MediaDataManager.Listener {
override fun onMediaDataLoaded(key: String, data: MediaData) {
updateViewVisibility()
@@ -66,4 +89,42 @@
hostView.visibility = if (visible) View.VISIBLE else View.GONE
visibleChangedListener?.invoke(visible)
}
+
+ class MediaHostState @Inject constructor() : MediaState {
+ override var expansion: Float = 0.0f
+ override var showsOnlyActiveMedia: Boolean = false
+ override val boundsOnScreen: Rect = Rect()
+
+ override fun copy() : MediaState {
+ val mediaHostState = MediaHostState()
+ mediaHostState.expansion = expansion
+ mediaHostState.showsOnlyActiveMedia = showsOnlyActiveMedia
+ mediaHostState.boundsOnScreen.set(boundsOnScreen)
+ return mediaHostState
+ }
+
+ override fun interpolate(other: MediaState, amount: Float) : MediaState {
+ val result = MediaHostState()
+ result.expansion = MathUtils.lerp(expansion, other.expansion, amount)
+ val left = MathUtils.lerp(boundsOnScreen.left.toFloat(),
+ other.boundsOnScreen.left.toFloat(), amount).toInt()
+ val top = MathUtils.lerp(boundsOnScreen.top.toFloat(),
+ other.boundsOnScreen.top.toFloat(), amount).toInt()
+ val right = MathUtils.lerp(boundsOnScreen.right.toFloat(),
+ other.boundsOnScreen.right.toFloat(), amount).toInt()
+ val bottom = MathUtils.lerp(boundsOnScreen.bottom.toFloat(),
+ other.boundsOnScreen.bottom.toFloat(), amount).toInt()
+ result.boundsOnScreen.set(left, top, right, bottom)
+ result.showsOnlyActiveMedia = other.showsOnlyActiveMedia || showsOnlyActiveMedia
+ return result
+ }
+ }
+}
+
+interface MediaState {
+ var expansion: Float
+ var showsOnlyActiveMedia: Boolean
+ val boundsOnScreen: Rect
+ fun copy() : MediaState
+ fun interpolate(other: MediaState, amount: Float) : MediaState
}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
index 088630c..6b0775f 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
@@ -167,7 +167,7 @@
int height = calculateContainerHeight();
setBottom(getTop() + height);
mQSDetail.setBottom(getTop() + height);
- // Pin QS Footer to the bottom of the panel.
+ // Pin the drag handle to the bottom of the panel.
mDragHandle.setTranslationY(height - mDragHandle.getHeight());
mBackground.setTop(mQSPanel.getTop());
mBackground.setBottom(height);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
index 5b09267..865fd07 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
@@ -37,6 +37,7 @@
import com.android.systemui.Interpolators;
import com.android.systemui.R;
import com.android.systemui.R.id;
+import com.android.systemui.media.MediaHost;
import com.android.systemui.plugins.qs.QS;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.qs.customize.QSCustomizer;
@@ -47,6 +48,7 @@
import com.android.systemui.statusbar.policy.RemoteInputQuickSettingsDisabler;
import com.android.systemui.util.InjectionInflationController;
import com.android.systemui.util.LifecycleFragment;
+import com.android.systemui.util.Utils;
import javax.inject.Inject;
@@ -91,6 +93,7 @@
*/
private int mState;
private QSContainerImplController mQSContainerImplController;
+ private int[] mTmpLocation = new int[2];
@Inject
public QSFragment(RemoteInputQuickSettingsDisabler remoteInputQsDisabler,
@@ -377,8 +380,7 @@
mLastKeyguardAndExpanded = onKeyguardAndExpanded;
boolean fullyExpanded = expansion == 1;
- int heightDiff = mQSPanel.getBottom() - mHeader.getBottom() + mHeader.getPaddingBottom()
- + mFooter.getHeight();
+ int heightDiff = mQSPanel.getBottom() - mHeader.getBottom() + mHeader.getPaddingBottom();
float panelTranslationY = translationScaleY * heightDiff;
// Let the views animate their contents correctly by giving them the necessary context.
@@ -404,6 +406,32 @@
if (mQSAnimator != null) {
mQSAnimator.setPosition(expansion);
}
+ updateMediaPositions();
+ }
+
+ private void updateMediaPositions() {
+ if (Utils.useQsMediaPlayer(getContext())) {
+ mContainer.getLocationOnScreen(mTmpLocation);
+ float absoluteBottomPosition = mTmpLocation[1] + mContainer.getHeight();
+ pinToBottom(absoluteBottomPosition, mQSPanel.getMediaHost());
+ pinToBottom(absoluteBottomPosition - mHeader.getPaddingBottom(),
+ mHeader.getHeaderQsPanel().getMediaHost());
+ }
+ }
+
+ private void pinToBottom(float absoluteBottomPosition, MediaHost mediaHost) {
+ View hostView = mediaHost.getHostView();
+ if (mLastQSExpansion > 0) {
+ ViewGroup.MarginLayoutParams params =
+ (ViewGroup.MarginLayoutParams) hostView.getLayoutParams();
+ float targetPosition = absoluteBottomPosition - params.bottomMargin
+ - hostView.getHeight();
+ float currentPosition = mediaHost.getCurrentState().getBoundsOnScreen().top
+ - hostView.getTranslationY();
+ hostView.setTranslationY(targetPosition - currentPosition);
+ } else {
+ hostView.setTranslationY(0);
+ }
}
private boolean headerWillBeAnimating() {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
index 0222969..3e87b02 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
@@ -104,7 +104,6 @@
private final MetricsLogger mMetricsLogger = Dependency.get(MetricsLogger.class);
private final QSTileRevealController mQsTileRevealController;
- private ActivityStarter mActivityStarter;
protected boolean mExpanded;
protected boolean mListening;
@@ -148,7 +147,6 @@
BroadcastDispatcher broadcastDispatcher,
QSLogger qsLogger,
MediaHost mediaHost,
- ActivityStarter activityStarter,
UiEventLogger uiEventLogger
) {
super(context, attrs);
@@ -157,7 +155,6 @@
mQSLogger = qsLogger;
mDumpManager = dumpManager;
mBroadcastDispatcher = broadcastDispatcher;
- mActivityStarter = activityStarter;
mUiEventLogger = uiEventLogger;
setOrientation(VERTICAL);
@@ -186,7 +183,6 @@
findViewById(R.id.brightness_slider), mBroadcastDispatcher);
}
-
@Override
protected void onFinishInflate() {
super.onFinishInflate();
@@ -198,7 +194,7 @@
protected void addMediaHostView() {
mMediaHost.init(MediaHierarchyManager.LOCATION_QS);
- mMediaHost.setExpanded(true);
+ mMediaHost.setExpansion(1.0f);
mMediaHost.setShowsOnlyActiveMedia(false);
ViewGroup hostView = mMediaHost.getHostView();
addView(hostView);
@@ -219,7 +215,8 @@
@Override
public void addTrack(MediaDescription desc, ComponentName component,
QSMediaBrowser browser) {
- if (component == null) {
+ // TODO: Fix Resumption b/156104922
+/* if (component == null) {
Log.e(TAG, "Component cannot be null");
return;
}
@@ -255,7 +252,7 @@
int iconColor = Color.DKGRAY;
int bgColor = Color.LTGRAY;
player.setMediaSession(token, desc, iconColor, bgColor, browser.getAppIntent(),
- pkgName);
+ pkgName);*/
}
};
@@ -856,6 +853,10 @@
}
}
+ public MediaHost getMediaHost() {
+ return mMediaHost;
+ }
+
private class H extends Handler {
private static final int SHOW_DETAIL = 1;
private static final int SET_TILE_VISIBILITY = 2;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java
index 194cd59..dfd385d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java
@@ -83,11 +83,10 @@
BroadcastDispatcher broadcastDispatcher,
QSLogger qsLogger,
MediaHost mediaHost,
- ActivityStarter activityStarter,
UiEventLogger uiEventLogger
) {
- super(context, attrs, dumpManager, broadcastDispatcher, qsLogger, mediaHost,
- activityStarter, uiEventLogger);
+ super(context, attrs, dumpManager, broadcastDispatcher, qsLogger, mediaHost,
+ uiEventLogger);
if (mFooter != null) {
removeView(mFooter.getView());
}
@@ -193,7 +192,7 @@
return null;
});
mMediaHost.init(MediaHierarchyManager.LOCATION_QQS);
- mMediaHost.setExpanded(false);
+ mMediaHost.setExpansion(0.0f);
mMediaHost.setShowsOnlyActiveMedia(true);
reAttachMediaHost();
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java
index 7901363..3b2bea8 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java
@@ -220,6 +220,10 @@
mNextAlarmTextView.setSelected(true);
}
+ public QuickQSPanel getHeaderQsPanel() {
+ return mHeaderQsPanel;
+ }
+
private List<String> getIgnoredIconSlots() {
ArrayList<String> ignored = new ArrayList<>();
ignored.add(mContext.getResources().getString(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java
index 5797944..0831c0b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java
@@ -767,6 +767,10 @@
return mContentTranslation;
}
+ public boolean wantsAddAndRemoveAnimations() {
+ return true;
+ }
+
/**
* A listener notifying when {@link #getActualHeight} changes.
*/
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MediaHeaderView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MediaHeaderView.java
index 82560f5..65594ef 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MediaHeaderView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MediaHeaderView.java
@@ -60,4 +60,9 @@
layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT;
layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT;
}
+
+ @Override
+ public boolean wantsAddAndRemoveAnimations() {
+ return false;
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index 7f32c00..db9faf5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -3074,6 +3074,9 @@
*/
@ShadeViewRefactor(RefactorComponent.STATE_RESOLVER)
private boolean generateRemoveAnimation(ExpandableView child) {
+ if (!child.wantsAddAndRemoveAnimations()) {
+ return false;
+ }
if (removeRemovedChildFromHeadsUpChangeAnimations(child)) {
mAddedHeadsUpChildren.remove(child);
return false;
@@ -3428,7 +3431,8 @@
@Override
@ShadeViewRefactor(RefactorComponent.STATE_RESOLVER)
public void generateAddAnimation(ExpandableView child, boolean fromMoreCard) {
- if (mIsExpanded && mAnimationsEnabled && !mChangePositionInProgress && !isFullyHidden()) {
+ if (mIsExpanded && mAnimationsEnabled && !mChangePositionInProgress && !isFullyHidden()
+ && child.wantsAddAndRemoveAnimations()) {
// Generate Animations
mChildrenToAddAnimated.add(child);
if (fromMoreCard) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
index aa6baff..8161a83 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
@@ -3518,7 +3518,11 @@
// Calculate quick setting heights.
int oldMaxHeight = mQsMaxExpansionHeight;
if (mQs != null) {
+ float previousMin = mQsMinExpansionHeight;
mQsMinExpansionHeight = mKeyguardShowing ? 0 : mQs.getQsMinExpansionHeight();
+ if (mQsExpansionHeight == previousMin) {
+ mQsExpansionHeight = mQsMinExpansionHeight;
+ }
mQsMaxExpansionHeight = mQs.getDesiredHeight();
mNotificationStackScroller.setMaxTopPadding(
mQsMaxExpansionHeight + mQsNotificationTopPadding);