From b98e9ac62121b465264499b1632a28137d8779de Mon Sep 17 00:00:00 2001 From: Selim Cinek Date: Wed, 15 Apr 2020 12:02:43 -0700 Subject: [PATCH 01/14] Made sure that we're only updating the media notification when inflated Previously the update was happening before inflation, but isMediaNotification() actually depended on it. Bug: 154137987 Test: add media notification fresh on lockscreen, without ever updating, observe media extracted to background Change-Id: I8f7ef11d5c99f2905c4942c07a701bbc134c1307 --- .../android/systemui/statusbar/NotificationMediaManager.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java index e32d174d7c771..459c95b82ba48 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java @@ -206,12 +206,12 @@ public class NotificationMediaManager implements Dumpable { mMainExecutor = mainExecutor; notificationEntryManager.addNotificationEntryListener(new NotificationEntryListener() { @Override - public void onPendingEntryAdded(NotificationEntry entry) { + public void onEntryInflated(NotificationEntry entry) { findAndUpdateMediaNotifications(); } @Override - public void onPreEntryUpdated(NotificationEntry entry) { + public void onEntryReinflated(NotificationEntry entry) { findAndUpdateMediaNotifications(); } From d835792bb061ad410a747c04b9a3ec7fd715960a Mon Sep 17 00:00:00 2001 From: Selim Cinek Date: Fri, 10 Apr 2020 15:06:53 -0700 Subject: [PATCH 02/14] Changing media to MotionLayout Bug: 154137987 Change-Id: Ice66b2bd79a837832cf1ea9bc3920690ac263a5a --- .../res/drawable/qs_media_background.xml | 2 +- .../SystemUI/res/layout/qs_media_panel.xml | 371 +++++++----------- packages/SystemUI/res/values/dimens.xml | 2 +- packages/SystemUI/res/xml/media_scene.xml | 312 +++++++++++++++ .../systemui/media/IlluminationDrawable.kt | 38 +- .../systemui/media/MediaControlPanel.java | 7 +- .../android/systemui/qs/QSMediaPlayer.java | 39 +- .../systemui/qs/QuickQSMediaPlayer.java | 2 +- 8 files changed, 507 insertions(+), 266 deletions(-) create mode 100644 packages/SystemUI/res/xml/media_scene.xml diff --git a/packages/SystemUI/res/drawable/qs_media_background.xml b/packages/SystemUI/res/drawable/qs_media_background.xml index e79c9a40918c5..80db3bec86c17 100644 --- a/packages/SystemUI/res/drawable/qs_media_background.xml +++ b/packages/SystemUI/res/drawable/qs_media_background.xml @@ -19,4 +19,4 @@ systemui:rippleMinSize="30dp" systemui:rippleMaxSize="135dp" systemui:highlight="15" - systemui:cornerRadius="@dimen/qs_media_corner_radius" /> \ No newline at end of file + systemui:cornerRadius="?android:attr/dialogCornerRadius" /> \ No newline at end of file diff --git a/packages/SystemUI/res/layout/qs_media_panel.xml b/packages/SystemUI/res/layout/qs_media_panel.xml index d633ff40df9e0..2b30096e2c042 100644 --- a/packages/SystemUI/res/layout/qs_media_panel.xml +++ b/packages/SystemUI/res/layout/qs_media_panel.xml @@ -16,236 +16,155 @@ --> - + android:gravity="center_horizontal|fill_vertical" + android:paddingBottom="@dimen/qs_media_panel_outer_padding" + app:layoutDescription="@xml/media_scene"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:visibility="gone" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 2cbb49801aa85..cce4195040c2c 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1232,7 +1232,7 @@ 8dp 16dp 10dp - 72dp + 52dp 20dp 8dp diff --git a/packages/SystemUI/res/xml/media_scene.xml b/packages/SystemUI/res/xml/media_scene.xml new file mode 100644 index 0000000000000..c625fbc4c80f1 --- /dev/null +++ b/packages/SystemUI/res/xml/media_scene.xml @@ -0,0 +1,312 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/SystemUI/src/com/android/systemui/media/IlluminationDrawable.kt b/packages/SystemUI/src/com/android/systemui/media/IlluminationDrawable.kt index 937472735bb04..7432165564349 100644 --- a/packages/SystemUI/src/com/android/systemui/media/IlluminationDrawable.kt +++ b/packages/SystemUI/src/com/android/systemui/media/IlluminationDrawable.kt @@ -6,6 +6,7 @@ import android.animation.AnimatorSet import android.animation.ValueAnimator import android.content.res.ColorStateList import android.content.res.Resources +import android.content.res.TypedArray import android.graphics.Canvas import android.graphics.Color import android.graphics.ColorFilter @@ -49,6 +50,7 @@ private data class RippleData( @Keep class IlluminationDrawable : Drawable() { + private var themeAttrs: IntArray? = null private var cornerRadius = 0f private var highlightColor = Color.TRANSPARENT private val rippleData = RippleData(0f, 0f, 0f, 0f, 0f, 0f, 0f) @@ -139,13 +141,41 @@ class IlluminationDrawable : Drawable() { theme: Resources.Theme? ) { val a = obtainAttributes(r, theme, attrs, R.styleable.IlluminationDrawable) - cornerRadius = a.getDimension(R.styleable.IlluminationDrawable_cornerRadius, cornerRadius) - rippleData.minSize = a.getDimension(R.styleable.IlluminationDrawable_rippleMinSize, 0f) - rippleData.maxSize = a.getDimension(R.styleable.IlluminationDrawable_rippleMaxSize, 0f) - rippleData.highlight = a.getInteger(R.styleable.IlluminationDrawable_highlight, 0) / 100f + themeAttrs = a.extractThemeAttrs() + updateStateFromTypedArray(a) a.recycle() } + private fun updateStateFromTypedArray(a: TypedArray) { + if (a.hasValue(R.styleable.IlluminationDrawable_cornerRadius)) { + cornerRadius = a.getDimension(R.styleable.IlluminationDrawable_cornerRadius, + cornerRadius) + } + if (a.hasValue(R.styleable.IlluminationDrawable_rippleMinSize)) { + rippleData.minSize = a.getDimension(R.styleable.IlluminationDrawable_rippleMinSize, 0f) + } + if (a.hasValue(R.styleable.IlluminationDrawable_rippleMaxSize)) { + rippleData.maxSize = a.getDimension(R.styleable.IlluminationDrawable_rippleMaxSize, 0f) + } + if (a.hasValue(R.styleable.IlluminationDrawable_highlight)) { + rippleData.highlight = a.getInteger(R.styleable.IlluminationDrawable_highlight, 0) / + 100f + } + } + + override fun canApplyTheme(): Boolean { + return themeAttrs != null && themeAttrs!!.size > 0 || super.canApplyTheme() + } + + override fun applyTheme(t: Resources.Theme) { + super.applyTheme(t) + themeAttrs?.let { + val a = t.resolveAttributes(it, R.styleable.IlluminationDrawable) + updateStateFromTypedArray(a) + a.recycle() + } + } + override fun setColorFilter(p0: ColorFilter?) { throw UnsupportedOperationException("Color filters are not supported") } diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java index 557132bdf08e7..9adfe7da44077 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java +++ b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java @@ -53,6 +53,7 @@ import android.widget.LinearLayout; import android.widget.TextView; import androidx.annotation.Nullable; +import androidx.constraintlayout.motion.widget.MotionLayout; import androidx.core.graphics.drawable.RoundedBitmapDrawable; import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory; @@ -70,7 +71,7 @@ import java.util.List; import java.util.concurrent.Executor; /** - * Base media control panel for System UI + * A view controller used for Media Playback. */ public class MediaControlPanel { private static final String TAG = "MediaControlPanel"; @@ -80,7 +81,7 @@ public class MediaControlPanel { private final ActivityStarter mActivityStarter; private Context mContext; - protected LinearLayout mMediaNotifView; + protected MotionLayout mMediaNotifView; private View mSeamless; private MediaSession.Token mToken; private MediaController mController; @@ -187,7 +188,7 @@ public class MediaControlPanel { ActivityStarter activityStarter) { mContext = context; LayoutInflater inflater = LayoutInflater.from(mContext); - mMediaNotifView = (LinearLayout) inflater.inflate(layoutId, parent, false); + mMediaNotifView = (MotionLayout) inflater.inflate(layoutId, parent, false); // 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 diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSMediaPlayer.java b/packages/SystemUI/src/com/android/systemui/qs/QSMediaPlayer.java index 174441bdf0657..0571c68fd7073 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSMediaPlayer.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSMediaPlayer.java @@ -37,7 +37,9 @@ import android.widget.LinearLayout; import android.widget.SeekBar; import android.widget.TextView; +import androidx.constraintlayout.widget.ConstraintSet; import com.android.settingslib.media.LocalMediaManager; +import com.android.internal.widget.MediaNotificationView; import com.android.systemui.R; import com.android.systemui.media.IlluminationDrawable; import com.android.systemui.media.MediaControlPanel; @@ -169,7 +171,7 @@ public class QSMediaPlayer extends MediaControlPanel { super.setMediaSession(token, icon, largeIcon, iconColor, bgColor, contentIntent, appName, key); - + ConstraintSet constraintSet = mMediaNotifView.getConstraintSet(R.id.expanded); // Media controls if (actionsContainer != null) { LinearLayout parentActionsLayout = (LinearLayout) actionsContainer; @@ -179,7 +181,7 @@ public class QSMediaPlayer extends MediaControlPanel { ImageButton thatBtn = parentActionsLayout.findViewById(NOTIF_ACTION_IDS[i]); if (thatBtn == null || thatBtn.getDrawable() == null || thatBtn.getVisibility() != View.VISIBLE) { - thisBtn.setVisibility(View.GONE); + constraintSet.setVisibility(QS_ACTION_IDS[i], ConstraintSet.GONE); continue; } @@ -190,7 +192,7 @@ public class QSMediaPlayer extends MediaControlPanel { Drawable thatIcon = thatBtn.getDrawable(); thisBtn.setImageDrawable(thatIcon.mutate()); - thisBtn.setVisibility(View.VISIBLE); + constraintSet.setVisibility(QS_ACTION_IDS[i], ConstraintSet.VISIBLE); thisBtn.setOnClickListener(v -> { Log.d(TAG, "clicking on other button"); thatBtn.performClick(); @@ -199,8 +201,7 @@ public class QSMediaPlayer extends MediaControlPanel { // Hide any unused buttons for (; i < QS_ACTION_IDS.length; i++) { - ImageButton thisBtn = mMediaNotifView.findViewById(QS_ACTION_IDS[i]); - thisBtn.setVisibility(View.GONE); + constraintSet.setVisibility(QS_ACTION_IDS[i], ConstraintSet.GONE); } } @@ -214,41 +215,19 @@ public class QSMediaPlayer extends MediaControlPanel { private void initLongPressMenu(int iconColor) { // Set up long press menu - View guts = mMediaNotifView.findViewById(R.id.media_guts); - View options = mMediaNotifView.findViewById(R.id.qs_media_controls_options); - options.setMinimumHeight(guts.getHeight()); - - View clearView = options.findViewById(R.id.remove); - clearView.setOnClickListener(b -> { - removePlayer(); - }); - ImageView removeIcon = options.findViewById(R.id.remove_icon); - removeIcon.setImageTintList(ColorStateList.valueOf(iconColor)); - TextView removeText = options.findViewById(R.id.remove_text); - removeText.setTextColor(iconColor); - - TextView cancelView = options.findViewById(R.id.cancel); - cancelView.setTextColor(iconColor); - cancelView.setOnClickListener(b -> { - options.setVisibility(View.GONE); - guts.setVisibility(View.VISIBLE); - }); - // ... but don't enable it yet, and make sure is reset when the session is updated - mMediaNotifView.setOnLongClickListener(null); - options.setVisibility(View.GONE); - guts.setVisibility(View.VISIBLE); + // TODO: b/156036025 bring back media guts } @Override protected void resetButtons() { super.resetButtons(); mSeekBarViewModel.clearController(); - View guts = mMediaNotifView.findViewById(R.id.media_guts); +// View guts = mMediaNotifView.findViewById(R.id.media_guts); View options = mMediaNotifView.findViewById(R.id.qs_media_controls_options); mMediaNotifView.setOnLongClickListener(v -> { // Replace player view with close/cancel view - guts.setVisibility(View.GONE); + // TODO: b/156036025 make this work options.setVisibility(View.VISIBLE); return true; // consumed click }); diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickQSMediaPlayer.java b/packages/SystemUI/src/com/android/systemui/qs/QuickQSMediaPlayer.java index 5cb75e60e22a9..7b53eed64ae6f 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QuickQSMediaPlayer.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QuickQSMediaPlayer.java @@ -54,7 +54,7 @@ public class QuickQSMediaPlayer extends MediaControlPanel { */ public QuickQSMediaPlayer(Context context, ViewGroup parent, Executor foregroundExecutor, Executor backgroundExecutor, ActivityStarter activityStarter) { - super(context, parent, null, R.layout.qqs_media_panel, QQS_ACTION_IDS, + super(context, parent, null, R.layout.qs_media_panel, QQS_ACTION_IDS, foregroundExecutor, backgroundExecutor, activityStarter); } From 5dbef2d99c4c16b33a8331bd656e4373c7ec85bf Mon Sep 17 00:00:00 2001 From: Selim Cinek Date: Thu, 7 May 2020 17:44:38 -0700 Subject: [PATCH 03/14] Refactored the Media Player management Previously all media instances had their own management and it could easily happen that certain players would get out of sync as a result. We now have a unified architecture to listen to media notifications and inflating a singular UI Test: atest SystemUiTests Bug: 154137987 Change-Id: I9f807e6431dd7cb54ca9b6d983379d770a281f31 --- .../res/layout/keyguard_media_header.xml | 105 ----- .../SystemUI/res/layout/media_carousel.xml | 3 +- .../android/keyguard/KeyguardMediaPlayer.java | 381 ------------------ .../keyguard/KeyguardSliceProvider.java | 3 +- .../systemui/media/KeyguardMediaController.kt | 67 +++ .../systemui/media/MediaControlPanel.java | 181 +++++++-- .../media/MediaData.kt} | 26 +- .../systemui/media/MediaDataManager.kt | 206 ++++++++++ .../systemui/media/MediaHierarchyManager.kt | 216 ++++++++++ .../com/android/systemui/qs/QSAnimator.java | 8 - .../android/systemui/qs/QSMediaPlayer.java | 262 ------------ .../src/com/android/systemui/qs/QSPanel.java | 226 +---------- .../systemui/qs/QuickQSMediaPlayer.java | 128 ------ .../com/android/systemui/qs/QuickQSPanel.java | 108 +++-- .../systemui/qs/QuickStatusBarHeader.java | 2 +- .../statusbar/NotificationMediaManager.java | 78 +++- .../dagger/StatusBarDependenciesModule.java | 10 +- .../collection/NotificationEntry.java | 4 +- .../NotifCollectionListener.java | 7 +- .../interruption/BypassHeadsUpNotifier.kt | 2 +- .../NotificationMediaTemplateViewWrapper.java | 32 -- .../notification/stack/MediaHeaderView.java | 10 +- .../stack/NotificationSectionsManager.java | 16 +- .../NotificationPanelViewController.java | 8 +- .../util/animation/UniqueObjectHost.kt | 41 ++ .../keyguard/KeyguardMediaPlayerTest.kt | 176 -------- .../keyguard/KeyguardSliceProviderTest.java | 10 +- .../NotificationSectionsManagerTest.java | 9 +- .../phone/NotificationPanelViewTest.java | 5 +- 29 files changed, 892 insertions(+), 1438 deletions(-) delete mode 100644 packages/SystemUI/src/com/android/keyguard/KeyguardMediaPlayer.java create mode 100644 packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt rename packages/SystemUI/src/com/android/{keyguard/KeyguardMedia.kt => systemui/media/MediaData.kt} (58%) create mode 100644 packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt create mode 100644 packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt delete mode 100644 packages/SystemUI/src/com/android/systemui/qs/QSMediaPlayer.java delete mode 100644 packages/SystemUI/src/com/android/systemui/qs/QuickQSMediaPlayer.java create mode 100644 packages/SystemUI/src/com/android/systemui/util/animation/UniqueObjectHost.kt delete mode 100644 packages/SystemUI/tests/src/com/android/keyguard/KeyguardMediaPlayerTest.kt diff --git a/packages/SystemUI/res/layout/keyguard_media_header.xml b/packages/SystemUI/res/layout/keyguard_media_header.xml index 20ec10ca1e1bb..a520719566ab8 100644 --- a/packages/SystemUI/res/layout/keyguard_media_header.xml +++ b/packages/SystemUI/res/layout/keyguard_media_header.xml @@ -45,109 +45,4 @@ android:layout_height="match_parent" /> - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/SystemUI/res/layout/media_carousel.xml b/packages/SystemUI/res/layout/media_carousel.xml index 149446c55fc5a..f5fc67d6856c1 100644 --- a/packages/SystemUI/res/layout/media_carousel.xml +++ b/packages/SystemUI/res/layout/media_carousel.xml @@ -22,11 +22,10 @@ android:layout_height="wrap_content" android:padding="@dimen/qs_media_padding" android:scrollbars="none" - android:visibility="gone" > diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardMediaPlayer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardMediaPlayer.java deleted file mode 100644 index af5196f92bcba..0000000000000 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardMediaPlayer.java +++ /dev/null @@ -1,381 +0,0 @@ -/* - * Copyright (C) 2020 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. - */ - -package com.android.keyguard; - -import android.app.Notification; -import android.app.PendingIntent; -import android.content.Context; -import android.content.res.ColorStateList; -import android.graphics.Bitmap; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.Icon; -import android.media.MediaMetadata; -import android.media.session.MediaController; -import android.media.session.MediaSession; -import android.util.Log; -import android.view.View; -import android.widget.ImageButton; -import android.widget.ImageView; -import android.widget.TextView; - -import androidx.core.graphics.drawable.RoundedBitmapDrawable; -import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory; -import androidx.lifecycle.LiveData; -import androidx.lifecycle.MutableLiveData; -import androidx.lifecycle.Observer; -import androidx.palette.graphics.Palette; - -import com.android.internal.util.ContrastColorUtil; -import com.android.systemui.R; -import com.android.systemui.dagger.qualifiers.Background; -import com.android.systemui.media.MediaControllerFactory; -import com.android.systemui.statusbar.notification.MediaNotificationProcessor; -import com.android.systemui.statusbar.notification.collection.NotificationEntry; -import com.android.systemui.statusbar.notification.stack.MediaHeaderView; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.Executor; - -import javax.inject.Inject; -import javax.inject.Singleton; - -/** - * Media controls to display on the lockscreen - * - * TODO: Should extend MediaControlPanel to avoid code duplication. - * Unfortunately, it isn't currently possible because the ActivatableNotificationView background is - * different. - */ -@Singleton -public class KeyguardMediaPlayer { - - private static final String TAG = "KeyguardMediaPlayer"; - // Buttons that can be displayed on lock screen media controls. - private static final int[] ACTION_IDS = {R.id.action0, R.id.action1, R.id.action2}; - - private final Context mContext; - private final Executor mBackgroundExecutor; - private final KeyguardMediaViewModel mViewModel; - private KeyguardMediaObserver mObserver; - - @Inject - public KeyguardMediaPlayer(Context context, MediaControllerFactory factory, - @Background Executor backgroundExecutor) { - mContext = context; - mBackgroundExecutor = backgroundExecutor; - mViewModel = new KeyguardMediaViewModel(context, factory); - } - - /** Binds media controls to a view hierarchy. */ - public void bindView(View v) { - if (mObserver != null) { - throw new IllegalStateException("cannot bind views, already bound"); - } - mViewModel.loadDimens(); - mObserver = new KeyguardMediaObserver(v); - // Control buttons - for (int i = 0; i < ACTION_IDS.length; i++) { - ImageButton button = v.findViewById(ACTION_IDS[i]); - if (button == null) { - continue; - } - final int index = i; - button.setOnClickListener(unused -> mViewModel.onActionClick(index)); - } - mViewModel.getKeyguardMedia().observeForever(mObserver); - } - - /** Unbinds media controls. */ - public void unbindView() { - if (mObserver == null) { - throw new IllegalStateException("cannot unbind views, nothing bound"); - } - mViewModel.getKeyguardMedia().removeObserver(mObserver); - mObserver = null; - } - - /** Clear the media controls because there isn't an active session. */ - public void clearControls() { - mBackgroundExecutor.execute(mViewModel::clearControls); - } - - /** - * Update the media player - * - * TODO: consider registering a MediaLister instead of exposing this update method. - * - * @param entry Media notification that will be used to update the player - * @param appIcon Icon for the app playing the media - * @param mediaMetadata Media metadata that will be used to update the player - */ - public void updateControls(NotificationEntry entry, Icon appIcon, - MediaMetadata mediaMetadata) { - if (mObserver == null) { - throw new IllegalStateException("cannot update controls, views not bound"); - } - if (mediaMetadata == null) { - Log.d(TAG, "media metadata was null, closing media controls"); - // Note that clearControls() executes on the same background executor, so there - // shouldn't be an issue with an outdated update running after clear. However, if stale - // controls are observed then consider removing any enqueued updates. - clearControls(); - return; - } - mBackgroundExecutor.execute(() -> mViewModel.updateControls(entry, appIcon, mediaMetadata)); - } - - /** ViewModel for KeyguardMediaControls. */ - private static final class KeyguardMediaViewModel { - - private final Context mContext; - private final MediaControllerFactory mMediaControllerFactory; - private final MutableLiveData mMedia = new MutableLiveData<>(); - private final Object mActionsLock = new Object(); - private List mActions; - private float mAlbumArtRadius; - private int mAlbumArtSize; - - KeyguardMediaViewModel(Context context, MediaControllerFactory factory) { - mContext = context; - mMediaControllerFactory = factory; - loadDimens(); - } - - /** Close the media player because there isn't an active session. */ - public void clearControls() { - synchronized (mActionsLock) { - mActions = null; - } - mMedia.postValue(null); - } - - /** Update the media player with information about the active session. */ - public void updateControls(NotificationEntry entry, Icon appIcon, - MediaMetadata mediaMetadata) { - - // Check the playback state of the media controller. If it is null, then the session was - // probably destroyed. Don't update in this case. - final MediaSession.Token token = entry.getSbn().getNotification().extras - .getParcelable(Notification.EXTRA_MEDIA_SESSION); - final MediaController controller = token != null - ? mMediaControllerFactory.create(token) : null; - if (controller != null && controller.getPlaybackState() == null) { - clearControls(); - return; - } - - // Foreground and Background colors computed from album art - Notification notif = entry.getSbn().getNotification(); - int fgColor = notif.color; - int bgColor = entry.getRow() == null ? -1 : entry.getRow().getCurrentBackgroundTint(); - Bitmap artworkBitmap = mediaMetadata.getBitmap(MediaMetadata.METADATA_KEY_ART); - if (artworkBitmap == null) { - artworkBitmap = mediaMetadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART); - } - if (artworkBitmap != null) { - // If we have art, get colors from that - Palette p = MediaNotificationProcessor.generateArtworkPaletteBuilder(artworkBitmap) - .generate(); - Palette.Swatch swatch = MediaNotificationProcessor.findBackgroundSwatch(p); - bgColor = swatch.getRgb(); - fgColor = MediaNotificationProcessor.selectForegroundColor(bgColor, p); - } - // Make sure colors will be legible - boolean isDark = !ContrastColorUtil.isColorLight(bgColor); - fgColor = ContrastColorUtil.resolveContrastColor(mContext, fgColor, bgColor, - isDark); - fgColor = ContrastColorUtil.ensureTextContrast(fgColor, bgColor, isDark); - - // Album art - RoundedBitmapDrawable artwork = null; - if (artworkBitmap != null) { - Bitmap original = artworkBitmap.copy(Bitmap.Config.ARGB_8888, true); - Bitmap scaled = Bitmap.createScaledBitmap(original, mAlbumArtSize, mAlbumArtSize, - false); - artwork = RoundedBitmapDrawableFactory.create(mContext.getResources(), scaled); - artwork.setCornerRadius(mAlbumArtRadius); - } - - // App name - Notification.Builder builder = Notification.Builder.recoverBuilder(mContext, notif); - String app = builder.loadHeaderAppName(); - - // App Icon - Drawable appIconDrawable = appIcon.loadDrawable(mContext); - - // Song name - String song = mediaMetadata.getString(MediaMetadata.METADATA_KEY_TITLE); - - // Artist name - String artist = mediaMetadata.getString(MediaMetadata.METADATA_KEY_ARTIST); - - // Control buttons - List actionIcons = new ArrayList<>(); - final List intents = new ArrayList<>(); - Notification.Action[] actions = notif.actions; - final int[] actionsToShow = notif.extras.getIntArray( - Notification.EXTRA_COMPACT_ACTIONS); - - Context packageContext = entry.getSbn().getPackageContext(mContext); - for (int i = 0; i < ACTION_IDS.length; i++) { - if (actionsToShow != null && actions != null && i < actionsToShow.length - && actionsToShow[i] < actions.length) { - final int idx = actionsToShow[i]; - actionIcons.add(actions[idx].getIcon().loadDrawable(packageContext)); - intents.add(actions[idx].actionIntent); - } else { - actionIcons.add(null); - intents.add(null); - } - } - synchronized (mActionsLock) { - mActions = intents; - } - - KeyguardMedia data = new KeyguardMedia(fgColor, bgColor, app, appIconDrawable, artist, - song, artwork, actionIcons); - mMedia.postValue(data); - } - - /** Gets state for the lock screen media controls. */ - public LiveData getKeyguardMedia() { - return mMedia; - } - - /** - * Handle user clicks on media control buttons (actions). - * - * @param index position of the button that was clicked. - */ - public void onActionClick(int index) { - PendingIntent intent = null; - // This might block the ui thread to wait for the lock. Currently, however, the - // lock is held by the bg thread to assign a member, which should be fast. An - // alternative could be to add the intents to the state and let the observer set - // the onClick listeners. - synchronized (mActionsLock) { - if (mActions != null && index < mActions.size()) { - intent = mActions.get(index); - } - } - if (intent != null) { - try { - intent.send(); - } catch (PendingIntent.CanceledException e) { - Log.d(TAG, "failed to send action intent", e); - } - } - } - - void loadDimens() { - mAlbumArtRadius = mContext.getResources().getDimension(R.dimen.qs_media_corner_radius); - mAlbumArtSize = (int) mContext.getResources().getDimension( - R.dimen.qs_media_album_size); - } - } - - /** Observer for state changes of lock screen media controls. */ - private static final class KeyguardMediaObserver implements Observer { - - private final View mRootView; - private final MediaHeaderView mMediaHeaderView; - private final ImageView mAlbumView; - private final ImageView mAppIconView; - private final TextView mAppNameView; - private final TextView mTitleView; - private final TextView mArtistView; - private final List mButtonViews = new ArrayList<>(); - - KeyguardMediaObserver(View v) { - mRootView = v; - mMediaHeaderView = v instanceof MediaHeaderView ? (MediaHeaderView) v : null; - mAlbumView = v.findViewById(R.id.album_art); - mAppIconView = v.findViewById(R.id.icon); - mAppNameView = v.findViewById(R.id.app_name); - mTitleView = v.findViewById(R.id.header_title); - mArtistView = v.findViewById(R.id.header_artist); - for (int i = 0; i < ACTION_IDS.length; i++) { - mButtonViews.add(v.findViewById(ACTION_IDS[i])); - } - } - - /** Updates lock screen media player views when state changes. */ - @Override - public void onChanged(KeyguardMedia data) { - if (data == null) { - mRootView.setVisibility(View.GONE); - return; - } - mRootView.setVisibility(View.VISIBLE); - - // Background color - if (mMediaHeaderView != null) { - mMediaHeaderView.setBackgroundColor(data.getBackgroundColor()); - } - - // Album art - if (mAlbumView != null) { - mAlbumView.setImageDrawable(data.getArtwork()); - mAlbumView.setVisibility(data.getArtwork() == null ? View.GONE : View.VISIBLE); - } - - // App icon - if (mAppIconView != null) { - Drawable iconDrawable = data.getAppIcon(); - iconDrawable.setTint(data.getForegroundColor()); - mAppIconView.setImageDrawable(iconDrawable); - } - - // App name - if (mAppNameView != null) { - String appNameString = data.getApp(); - mAppNameView.setText(appNameString); - mAppNameView.setTextColor(data.getForegroundColor()); - } - - // Song name - if (mTitleView != null) { - mTitleView.setText(data.getSong()); - mTitleView.setTextColor(data.getForegroundColor()); - } - - // Artist name - if (mArtistView != null) { - mArtistView.setText(data.getArtist()); - mArtistView.setTextColor(data.getForegroundColor()); - } - - // Control buttons - for (int i = 0; i < ACTION_IDS.length; i++) { - ImageButton button = mButtonViews.get(i); - if (button == null) { - continue; - } - Drawable icon = data.getActionIcons().get(i); - if (icon == null) { - button.setVisibility(View.GONE); - button.setImageDrawable(null); - } else { - button.setVisibility(View.VISIBLE); - button.setImageDrawable(icon); - button.setImageTintList(ColorStateList.valueOf(data.getForegroundColor())); - } - } - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java index 96494cfe640f6..3a37c0fd4634d 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java @@ -451,7 +451,8 @@ public class KeyguardSliceProvider extends SliceProvider implements * @param metadata New metadata. */ @Override - public void onMetadataOrStateChanged(MediaMetadata metadata, @PlaybackState.State int state) { + public void onPrimaryMetadataOrStateChanged(MediaMetadata metadata, + @PlaybackState.State int state) { synchronized (this) { boolean nextVisible = NotificationMediaManager.isPlayingState(state); mMediaHandler.removeCallbacksAndMessages(null); diff --git a/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt b/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt new file mode 100644 index 0000000000000..f60c012c4c2c4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2020 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. + */ + +package com.android.systemui.media + +import android.view.View +import com.android.internal.util.ContrastColorUtil +import com.android.systemui.statusbar.NotificationMediaManager +import com.android.systemui.statusbar.notification.stack.MediaHeaderView +import com.android.systemui.statusbar.phone.KeyguardBypassController +import javax.inject.Inject +import javax.inject.Singleton + +/** + * A class that controls the media notifications on the lock screen, handles its visibility and + * is responsible for the embedding of he media experience. + */ +@Singleton +class KeyguardMediaController @Inject constructor( + private val mediaHierarchyManager: MediaHierarchyManager, + private val notifMediaManager: NotificationMediaManager, + private val bypassController: KeyguardBypassController +) { + private var view: MediaHeaderView? = null + + init { + notifMediaManager.addCallback(object : NotificationMediaManager.MediaListener { + override fun onMediaDataLoaded(key: String, data: MediaData) { + updateVisibility() + } + + override fun onMediaDataRemoved(key: String) { + updateVisibility() + } + }) + } + + /** + * Attach this controller to a media view, initializing its state + */ + fun attach(mediaControlsView: MediaHeaderView) { + view = mediaControlsView + val hostView = mediaHierarchyManager.createMediaHost( + MediaHierarchyManager.LOCATION_LOCKSCREEN) + mediaControlsView.setMediaHost(hostView) + updateVisibility() + } + + private fun updateVisibility() { + val shouldBeVisible = notifMediaManager.hasActiveMedia() && !bypassController.bypassEnabled + view?.visibility = if (shouldBeVisible) View.VISIBLE else View.GONE + } +} + diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java index 9adfe7da44077..0297c90a29b9c 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java +++ b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java @@ -16,6 +16,8 @@ package com.android.systemui.media; +import static com.android.systemui.util.SysuiLifecycle.viewAttachLifecycle; + import android.annotation.LayoutRes; import android.app.PendingIntent; import android.content.ComponentName; @@ -28,9 +30,9 @@ 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.Icon; import android.graphics.drawable.RippleDrawable; import android.media.MediaDescription; import android.media.MediaMetadata; @@ -50,10 +52,12 @@ import android.view.ViewGroup; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.LinearLayout; +import android.widget.SeekBar; import android.widget.TextView; import androidx.annotation.Nullable; import androidx.constraintlayout.motion.widget.MotionLayout; +import androidx.constraintlayout.widget.ConstraintSet; import androidx.core.graphics.drawable.RoundedBitmapDrawable; import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory; @@ -65,6 +69,9 @@ import com.android.systemui.R; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.qs.QSMediaBrowser; import com.android.systemui.util.Assert; +import com.android.systemui.util.concurrency.DelayableExecutor; + +import org.jetbrains.annotations.NotNull; import java.io.IOException; import java.util.List; @@ -76,6 +83,18 @@ import java.util.concurrent.Executor; public class MediaControlPanel { private static final String TAG = "MediaControlPanel"; @Nullable private final LocalMediaManager mLocalMediaManager; + + // Button IDs for QS controls + static final int[] ACTION_IDS = { + R.id.action0, + R.id.action1, + R.id.action2, + R.id.action3, + R.id.action4 + }; + + private final SeekBarViewModel mSeekBarViewModel; + private final SeekBarObserver mSeekBarObserver; private final Executor mForegroundExecutor; protected final Executor mBackgroundExecutor; private final ActivityStarter mActivityStarter; @@ -92,8 +111,6 @@ public class MediaControlPanel { private boolean mIsRegistered = false; private String mKey; - private final int[] mActionIds; - public static final String MEDIA_PREFERENCES = "media_control_prefs"; public static final String MEDIA_PREFERENCE_KEY = "browser_components"; private SharedPreferences mSharedPrefs; @@ -101,15 +118,6 @@ public class MediaControlPanel { private boolean mIsRemotePlayback; private QSMediaBrowser mQSMediaBrowser; - // Button IDs used in notifications - protected static final int[] NOTIF_ACTION_IDS = { - com.android.internal.R.id.action0, - com.android.internal.R.id.action1, - com.android.internal.R.id.action2, - com.android.internal.R.id.action3, - com.android.internal.R.id.action4 - }; - // URI fields to try loading album art from private static final String[] ART_URIS = { MediaMetadata.METADATA_KEY_ALBUM_ART_URI, @@ -183,12 +191,11 @@ public class MediaControlPanel { * @param activityStarter activity starter */ public MediaControlPanel(Context context, ViewGroup parent, - @Nullable LocalMediaManager routeManager, @LayoutRes int layoutId, int[] actionIds, - Executor foregroundExecutor, Executor backgroundExecutor, - ActivityStarter activityStarter) { + @Nullable LocalMediaManager routeManager, Executor foregroundExecutor, + DelayableExecutor backgroundExecutor, ActivityStarter activityStarter) { mContext = context; LayoutInflater inflater = LayoutInflater.from(mContext); - mMediaNotifView = (MotionLayout) inflater.inflate(layoutId, parent, false); + mMediaNotifView = (MotionLayout) inflater.inflate(R.layout.qs_media_panel, parent, false); // 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 @@ -196,20 +203,41 @@ public class MediaControlPanel { // mStateListener to be unregistered in detach. mMediaNotifView.addOnAttachStateChangeListener(mStateListener); mLocalMediaManager = routeManager; - mActionIds = actionIds; mForegroundExecutor = foregroundExecutor; mBackgroundExecutor = backgroundExecutor; mActivityStarter = activityStarter; + mSeekBarViewModel = new SeekBarViewModel(backgroundExecutor); + mSeekBarObserver = new SeekBarObserver(getView()); + // Can't use the viewAttachLifecycle of media player because remove/add is used to adjust + // priority of players. As soon as it is removed, the lifecycle will end and the seek bar + // will stop updating. So, use the lifecycle of the parent instead. + // TODO: this parent is also detached, need to fix that + mSeekBarViewModel.getProgress().observe(viewAttachLifecycle(parent), mSeekBarObserver); + SeekBar bar = getView().findViewById(R.id.media_progress_bar); + bar.setOnSeekBarChangeListener(mSeekBarViewModel.getSeekBarListener()); + bar.setOnTouchListener(mSeekBarViewModel.getSeekBarTouchListener()); } /** * Get the view used to display media controls * @return the view */ - public View getView() { + public MotionLayout getView() { return mMediaNotifView; } + /** + * Sets the listening state of the player. + * + * Should be set to true when the QS panel is open. Otherwise, false. This is a signal to avoid + * unnecessary work when the QS panel is closed. + * + * @param listening True when player should be active. Otherwise, false. + */ + public void setListening(boolean listening) { + mSeekBarViewModel.setListening(listening); + } + /** * Get the context * @return context @@ -219,20 +247,12 @@ public class MediaControlPanel { } /** - * Update the media panel view for the given media session - * @param token - * @param iconDrawable - * @param largeIcon - * @param iconColor - * @param bgColor - * @param contentIntent - * @param appNameString - * @param key + * Bind this view based on the data given */ - public void setMediaSession(MediaSession.Token token, Drawable iconDrawable, Icon largeIcon, - int iconColor, int bgColor, PendingIntent contentIntent, String appNameString, - String key) { - // Ensure that component names are updated if token has changed + public void bind(@NotNull MediaData data) { + mToken = data.getToken(); + mForegroundColor = data.getForegroundColor(); + mBackgroundColor = data.getBackgroundColor(); if (mToken == null || !mToken.equals(token)) { if (mQSMediaBrowser != null) { Log.d(TAG, "Disconnecting old media browser"); @@ -244,8 +264,6 @@ public class MediaControlPanel { mCheckedForResumption = false; } - mForegroundColor = iconColor; - mBackgroundColor = bgColor; mController = new MediaController(mContext, mToken); mKey = key; @@ -258,6 +276,7 @@ public class MediaControlPanel { PackageManager pm = mContext.getPackageManager(); Intent resumeIntent = new Intent(MediaBrowserService.SERVICE_INTERFACE); List resumeInfo = pm.queryIntentServices(resumeIntent, 0); + // TODO: look into this resumption if (resumeInfo != null) { for (ResolveInfo inf : resumeInfo) { if (inf.serviceInfo.packageName.equals(mController.getPackageName())) { @@ -272,20 +291,33 @@ public class MediaControlPanel { mController.registerCallback(mSessionCallback); + albumView.setImageDrawable(data.getArtwork()); + mMediaNotifView.setBackgroundTintList(ColorStateList.valueOf(mBackgroundColor)); // Click action - if (contentIntent != null) { + PendingIntent clickIntent = data.getClickIntent(); + if (clickIntent != null) { mMediaNotifView.setOnClickListener(v -> { - mActivityStarter.postStartActivityDismissingKeyguard(contentIntent); + mActivityStarter.postStartActivityDismissingKeyguard(clickIntent); }); } // App icon - ImageView appIcon = mMediaNotifView.findViewById(R.id.icon); + ImageView appIcon = mMediaNotifView.requireViewById(R.id.icon); + // TODO: look at iconDrawable + Drawable iconDrawable = data.getAppIcon(); iconDrawable.setTint(mForegroundColor); appIcon.setImageDrawable(iconDrawable); + TextView titleText = mMediaNotifView.requireViewById(R.id.header_title); + titleText.setText(data.getSong()); + TextView appName = mMediaNotifView.requireViewById(R.id.app_name); + appName.setText(data.getApp()); + appName.setTextColor(mForegroundColor); + 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) { @@ -313,6 +345,54 @@ public class MediaControlPanel { mIsRemotePlayback = false; } + ConstraintSet expandedSet = mMediaNotifView.getConstraintSet(R.id.expanded); + ConstraintSet collapsedSet = mMediaNotifView.getConstraintSet(R.id.collapsed); + List actionsWhenCollapsed = data.getActionsToShowInCompact(); + // Media controls + int i = 0; + List actionIcons = data.getActions(); + for (; i < actionIcons.size() && i < ACTION_IDS.length; i++) { + final ImageButton button = mMediaNotifView.findViewById(ACTION_IDS[i]); + 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()) + .setupTouch(button, mMediaNotifView); + } + + button.setOnClickListener(v -> { + if (actionIntent != null) { + try { + actionIntent.send(); + } catch (PendingIntent.CanceledException e) { + e.printStackTrace(); + } + } + }); + boolean visibleInCompat = actionsWhenCollapsed.contains(i); + collapsedSet.setVisibility(ACTION_IDS[i], + visibleInCompat ? ConstraintSet.VISIBLE : ConstraintSet.GONE); + expandedSet.setVisibility(ACTION_IDS[i], ConstraintSet.VISIBLE); + } + + // Hide any unused buttons + for (; i < ACTION_IDS.length; i++) { + expandedSet.setVisibility(ACTION_IDS[i], ConstraintSet.GONE); + collapsedSet.setVisibility(ACTION_IDS[i], ConstraintSet.GONE); + } + + // Seek Bar + final MediaController controller = new MediaController(getContext(), data.getToken()); + mBackgroundExecutor.execute( + () -> mSeekBarViewModel.updateController(controller, data.getForegroundColor())); + + // Set up long press menu + // TODO: b/156036025 bring back media guts + makeActive(); // App title (not in mini player) @@ -614,15 +694,16 @@ public class MediaControlPanel { */ protected void resetButtons() { // Hide all the old buttons - for (int i = 0; i < mActionIds.length; i++) { - ImageButton thisBtn = mMediaNotifView.findViewById(mActionIds[i]); - if (thisBtn != null) { - thisBtn.setVisibility(View.GONE); - } + + ConstraintSet expandedSet = mMediaNotifView.getConstraintSet(R.id.expanded); + ConstraintSet collapsedSet = mMediaNotifView.getConstraintSet(R.id.collapsed); + for (int i = 1; i < ACTION_IDS.length; i++) { + expandedSet.setVisibility(ACTION_IDS[i], ConstraintSet.GONE); + collapsedSet.setVisibility(ACTION_IDS[i], ConstraintSet.GONE); } // Add a restart button - ImageButton btn = mMediaNotifView.findViewById(mActionIds[0]); + ImageButton btn = mMediaNotifView.findViewById(ACTION_IDS[0]); btn.setOnClickListener(v -> { Log.d(TAG, "Attempting to restart session"); if (mQSMediaBrowser != null) { @@ -644,7 +725,20 @@ public class MediaControlPanel { }); btn.setImageDrawable(mContext.getResources().getDrawable(R.drawable.lb_ic_play)); btn.setImageTintList(ColorStateList.valueOf(mForegroundColor)); - btn.setVisibility(View.VISIBLE); + expandedSet.setVisibility(ACTION_IDS[0], ConstraintSet.VISIBLE); + collapsedSet.setVisibility(ACTION_IDS[0], ConstraintSet.VISIBLE); + + mSeekBarViewModel.clearController(); + // TODO: fix guts + // View guts = mMediaNotifView.findViewById(R.id.media_guts); + View options = mMediaNotifView.findViewById(R.id.qs_media_controls_options); + + mMediaNotifView.setOnLongClickListener(v -> { + // Replace player view with close/cancel view +// guts.setVisibility(View.GONE); + options.setVisibility(View.VISIBLE); + return true; // consumed click + }); } private void makeActive() { @@ -668,7 +762,6 @@ public class MediaControlPanel { mIsRegistered = false; } } - /** * Verify that we can connect to the given component with a MediaBrowser, and if so, add that * component to the list of resumption components diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardMedia.kt b/packages/SystemUI/src/com/android/systemui/media/MediaData.kt similarity index 58% rename from packages/SystemUI/src/com/android/keyguard/KeyguardMedia.kt rename to packages/SystemUI/src/com/android/systemui/media/MediaData.kt index 487c29573a146..8165362ef677c 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardMedia.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaData.kt @@ -11,17 +11,18 @@ * 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. + * limitations under the License */ -package com.android.keyguard +package com.android.systemui.media +import android.app.PendingIntent import android.graphics.drawable.Drawable +import android.media.session.MediaSession -import java.util.List - -/** State for lock screen media controls. */ -data class KeyguardMedia( +/** State of a media view. */ +data class MediaData( + val initialized: Boolean = false, val foregroundColor: Int, val backgroundColor: Int, val app: String?, @@ -29,5 +30,16 @@ data class KeyguardMedia( val artist: String?, val song: String?, val artwork: Drawable?, - val actionIcons: List + val actions: List, + val actionsToShowInCompact: List, + val packageName: String?, + val token: MediaSession.Token?, + val clickIntent: PendingIntent? +) + +/** State of a media action. */ +data class MediaAction( + val drawable: Drawable?, + val intent: PendingIntent?, + val contentDescription: CharSequence? ) diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt new file mode 100644 index 0000000000000..298a5fd60813a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt @@ -0,0 +1,206 @@ +/* + * Copyright (C) 2020 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. + */ + +package com.android.systemui.media + +import android.app.Notification +import android.content.Context +import android.graphics.Bitmap +import android.graphics.drawable.Drawable +import android.media.MediaMetadata +import android.media.session.MediaSession +import android.provider.Settings +import android.service.notification.StatusBarNotification +import androidx.core.graphics.drawable.RoundedBitmapDrawable +import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory +import com.android.internal.util.ContrastColorUtil +import com.android.systemui.R +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.statusbar.NotificationMediaManager +import com.android.systemui.statusbar.notification.MediaNotificationProcessor +import java.util.* +import java.util.concurrent.Executor +import javax.inject.Inject +import javax.inject.Singleton +import kotlin.collections.LinkedHashMap + +/** + * A class that facilitates management and loading of Media Data, ready for binding. + */ +@Singleton +class MediaDataManager @Inject constructor( + private val context: Context, + private val mediaControllerFactory: MediaControllerFactory, + @Background private val backgroundExecutor: Executor, + @Main private val foregroundExcecutor: Executor +) { + + lateinit var listener: NotificationMediaManager.MediaListener + private var albumArtSize: Int = 0 + private var albumArtRadius: Int = 0 + private val mediaEntries: LinkedHashMap = LinkedHashMap() + + init { + loadDimens() + } + + private fun loadDimens() { + albumArtRadius = context.resources.getDimensionPixelSize(R.dimen.qs_media_corner_radius) + albumArtSize = context.resources.getDimensionPixelSize(R.dimen.qs_media_album_size) + } + + fun onNotificationAdded(key: String, sbn: StatusBarNotification) { + if (isMediaNotification(sbn)) { + if (!mediaEntries.containsKey(key)) { + mediaEntries.put(key, LOADING) + } + loadMediaData(key, sbn) + } else { + onNotificationRemoved(key) + } + } + + private fun loadMediaData(key: String, sbn: StatusBarNotification) { + backgroundExecutor.execute { + loadMediaDataInBg(key, sbn) + } + } + + private fun loadMediaDataInBg(key: String, sbn: StatusBarNotification) { + val token = sbn.notification.extras.getParcelable(Notification.EXTRA_MEDIA_SESSION) + as MediaSession.Token? + val metadata = mediaControllerFactory.create(token).metadata + + if (metadata == null) { + // TODO: handle this better, removing media notification + return + } + + // Foreground and Background colors computed from album art + val notif: Notification = sbn.notification + var fgColor = notif.color + var bgColor = -1 + var artworkBitmap = metadata.getBitmap(MediaMetadata.METADATA_KEY_ART) + if (artworkBitmap == null) { + artworkBitmap = metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART) + } + // TODO: load media data from uri + if (artworkBitmap != null) { + // If we have art, get colors from that + val p = MediaNotificationProcessor.generateArtworkPaletteBuilder(artworkBitmap) + .generate() + val swatch = MediaNotificationProcessor.findBackgroundSwatch(p) + bgColor = swatch.rgb + fgColor = MediaNotificationProcessor.selectForegroundColor(bgColor, p) + } + // Make sure colors will be legible + val isDark = !ContrastColorUtil.isColorLight(bgColor) + fgColor = ContrastColorUtil.resolveContrastColor(context, fgColor, bgColor, + isDark) + fgColor = ContrastColorUtil.ensureTextContrast(fgColor, bgColor, isDark) + + // Album art + var artwork: RoundedBitmapDrawable? = null + if (artworkBitmap != null) { + val original = artworkBitmap.copy(Bitmap.Config.ARGB_8888, true) + val scaled = Bitmap.createScaledBitmap(original, albumArtSize, albumArtSize, + false) + artwork = RoundedBitmapDrawableFactory.create(context.resources, scaled) + artwork.cornerRadius = albumArtRadius.toFloat() + } + + // App name + val builder = Notification.Builder.recoverBuilder(context, notif) + val app = builder.loadHeaderAppName() + + // App Icon + val smallIconDrawable: Drawable = sbn.notification.smallIcon.loadDrawable(context) + + // Song name + val song: String = metadata.getString(MediaMetadata.METADATA_KEY_TITLE) + + // Artist name + val artist: String = metadata.getString(MediaMetadata.METADATA_KEY_ARTIST) + + // Control buttons + val actionIcons: MutableList = ArrayList() + val actions = notif.actions + val actionsToShowCollapsed = notif.extras.getIntArray( + Notification.EXTRA_COMPACT_ACTIONS)?.toList() ?: emptyList() + // TODO: b/153736623 look into creating actions when this isn't a media style notification + + val packageContext: Context = sbn.getPackageContext(context) + for (action in actions) { + val mediaAction = MediaAction( + action.getIcon().loadDrawable(packageContext), + action.actionIntent, + action.title) + actionIcons.add(mediaAction) + } + + foregroundExcecutor.execute { + onMediaDataLoaded(key, MediaData(true, fgColor, bgColor, app, smallIconDrawable, artist, + song, artwork, actionIcons, actionsToShowCollapsed, sbn.packageName, token, + notif.contentIntent)) + } + + } + + fun onMediaDataLoaded(key: String, data: MediaData) { + if (mediaEntries.containsKey(key)) { + // Otherwise this was removed already + mediaEntries.put(key, data) + listener.onMediaDataLoaded(key, data) + } + } + + fun onNotificationRemoved(key: String) { + val removed = mediaEntries.remove(key) + if (removed != null) { + listener.onMediaDataRemoved(key) + } + } + + private fun isMediaNotification(sbn: StatusBarNotification) : Boolean { + if (!useUniversalMediaPlayer()) { + return false + } + if (!sbn.notification.hasMediaSession()) { + return false + } + val notificationStyle = sbn.notification.notificationStyle + if (Notification.DecoratedMediaCustomViewStyle::class.java.equals(notificationStyle) + || Notification.MediaStyle::class.java.equals(notificationStyle)) { + return true + } + return false + } + + /** + * are we using the universal media player + */ + private fun useUniversalMediaPlayer() + = Settings.System.getInt(context.contentResolver, "qs_media_player", 1) > 0 + + /** + * Are there any media notifications active? + */ + fun hasActiveMedia() = mediaEntries.size > 0 +} + +private val LOADING = MediaData(false, 0, 0, null, null, null, null, null, + emptyList(), emptyList(), null, null, null) diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt new file mode 100644 index 0000000000000..4a650ab828bc8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt @@ -0,0 +1,216 @@ +/* + * Copyright (C) 2020 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. + */ + +package com.android.systemui.media + +import android.annotation.IntDef +import android.content.Context +import android.view.LayoutInflater +import android.view.ViewGroup +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.R +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.statusbar.NotificationMediaManager +import com.android.systemui.statusbar.StatusBarState +import com.android.systemui.statusbar.notification.VisualStabilityManager +import com.android.systemui.statusbar.phone.KeyguardBypassController +import com.android.systemui.util.animation.UniqueObjectHost +import com.android.systemui.util.concurrency.DelayableExecutor +import java.util.concurrent.Executor +import javax.inject.Inject +import javax.inject.Singleton + +/** + * This manager is responsible for placement of the unique media view between the different hosts + * and animate the positions of the views to achieve seamless transitions. + */ +@Singleton +class MediaHierarchyManager @Inject constructor( + private val context: Context, + @Main private val foregroundExecutor: Executor, + @Background private val backgroundExecutor: DelayableExecutor, + private val localBluetoothManager: LocalBluetoothManager?, + private val visualStabilityManager: VisualStabilityManager, + private val statusBarStateController: StatusBarStateController, + private val bypassController: KeyguardBypassController, + mediaManager: NotificationMediaManager +) { + private val mediaCarousel: ViewGroup + private val mediaContent: ViewGroup + private val mediaPlayers: MutableMap = mutableMapOf() + private val mediaHosts = arrayOfNulls(LOCATION_LOCKSCREEN + 1) + private val visualStabilityCallback = ::reorderAllPlayers + private var currentAttachmentLocation = -1 + + var shouldListen = true + set(value) { + field = value + for (player in mediaPlayers.values) { + player.setListening(shouldListen) + } + } + + init { + mediaCarousel = inflateMediaCarousel() + mediaContent = mediaCarousel.findViewById(R.id.media_carousel) + mediaManager.addCallback(object : NotificationMediaManager.MediaListener { + override fun onMediaDataLoaded(key: String, data: MediaData) { + updateView(key, data) + } + + override fun onMediaDataRemoved(key: String) { + val removed = mediaPlayers.remove(key) + removed?.apply { + mediaContent.removeView(removed.view) + } + } + }) + } + + private fun inflateMediaCarousel(): ViewGroup { + return LayoutInflater.from(context).inflate( + R.layout.media_carousel, UniqueObjectHost(context), false) as ViewGroup + } + + private fun reorderAllPlayers() { + for (mediaPlayer in mediaPlayers.values) { + val view = mediaPlayer.view + if (mediaPlayer.isPlaying && mediaContent.indexOfChild(view) != 0) { + mediaContent.removeView(view) + mediaContent.addView(view, 0) + } + } + } + + private fun updateView(key: String, data: MediaData) { + var existingPlayer = mediaPlayers[key] + if (existingPlayer == null) { + // Set up listener for device changes + // TODO: integrate with MediaTransferManager? + val imm = InfoMediaManager(context, data.packageName, + null /* notification */, localBluetoothManager) + val routeManager = LocalMediaManager(context, localBluetoothManager, + imm, data.packageName) + + existingPlayer = MediaControlPanel(context, mediaContent, routeManager, + foregroundExecutor, backgroundExecutor) + mediaPlayers[key] = existingPlayer + val padding = context.resources.getDimensionPixelSize(R.dimen.qs_media_padding) + val lp = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT) + lp.marginStart = padding + lp.marginEnd = padding + existingPlayer.view.setLayoutParams(lp) + existingPlayer.setListening(shouldListen) + if (existingPlayer.isPlaying) { + mediaContent.addView(existingPlayer.view, 0) + } else { + mediaContent.addView(existingPlayer.view) + } + } else if (existingPlayer.isPlaying && + mediaContent.indexOfChild(existingPlayer.view) != 0) { + if (visualStabilityManager.isReorderingAllowed) { + mediaContent.removeView(existingPlayer.view) + mediaContent.addView(existingPlayer.view, 0) + } else { + visualStabilityManager.addReorderingAllowedCallback(visualStabilityCallback) + } + } + existingPlayer.bind(data) + } + + /** + * Create a media host view which can be attached to a view hierarchy and where the player + * will be placed in when the host is the currently desired state. + * + * @return the hostView associated with this location + */ + fun createMediaHost(@MediaLocation location: Int) : ViewGroup { + val viewHost = UniqueObjectHost(context) + mediaHosts[location] = viewHost + if (location == currentAttachmentLocation) { + // 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 + currentAttachmentLocation = -1 + } + updateAttachmentLocation(animate = false) + return viewHost + } + + private fun updateAttachmentLocation(animate: Boolean) { + var desiredLocation = calculateLocation() + if (desiredLocation != currentAttachmentLocation) { + val host = mediaHosts[desiredLocation] + host?.apply { + if (currentAttachmentLocation >= 0) { + val currentHost = mediaHosts[currentAttachmentLocation] + currentHost?.removeView(mediaCarousel) + } + host.addView(mediaCarousel) + // TODO: handle animation / repositioning etc + currentAttachmentLocation = desiredLocation + } + } + } + + @MediaLocation + private fun calculateLocation() : Int { + val onLockscreen = (!bypassController.bypassEnabled + && (statusBarStateController.state == StatusBarState.KEYGUARD + || statusBarStateController.state == StatusBarState.FULLSCREEN_USER_SWITCHER)) + return when { + qsExpansion > 0.0f -> LOCATION_QS + onLockscreen -> LOCATION_LOCKSCREEN + else -> LOCATION_QQS + } + } + + /** + * The expansion of quick settings + */ + var qsExpansion: Float = 0.0f + set(value) { + field = value + updateAttachmentLocation(animate = false) + } + + @IntDef(prefix = ["LOCATION_"], value = [LOCATION_QS, LOCATION_QQS, LOCATION_LOCKSCREEN]) + @Retention(AnnotationRetention.SOURCE) + annotation class MediaLocation + + companion object { + /** + * Attached in expanded quick settings + */ + const val LOCATION_QS = 0 + + /** + * Attached in the collapsed QS + */ + const val LOCATION_QQS = 1 + + /** + * Attached on the lock screen + */ + const val LOCATION_LOCKSCREEN = 2 + } +} \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java b/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java index a0ea7fae493db..ce002297e1a1f 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java @@ -269,14 +269,6 @@ public class QSAnimator implements Callback, PageListener, Listener, OnLayoutCha count++; } - - if (Utils.useQsMediaPlayer(mQsPanel.getContext())) { - View qsMediaView = mQsPanel.getMediaPanel(); - View qqsMediaView = mQuickQsPanel.getMediaPlayer().getView(); - translationXBuilder.addFloat(qsMediaView, "alpha", 0, 1); - translationXBuilder.addFloat(qqsMediaView, "alpha", 1, 0); - } - if (mAllowFancy) { // Make brightness appear static position and alpha in through second half. View brightness = mQsPanel.getBrightnessView(); diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSMediaPlayer.java b/packages/SystemUI/src/com/android/systemui/qs/QSMediaPlayer.java deleted file mode 100644 index 0571c68fd7073..0000000000000 --- a/packages/SystemUI/src/com/android/systemui/qs/QSMediaPlayer.java +++ /dev/null @@ -1,262 +0,0 @@ -/* - * Copyright (C) 2019 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. - */ - -package com.android.systemui.qs; - -import static com.android.systemui.util.SysuiLifecycle.viewAttachLifecycle; - -import android.app.PendingIntent; -import android.content.Context; -import android.content.pm.PackageManager; -import android.content.res.ColorStateList; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.Icon; -import android.media.MediaDescription; -import android.media.session.MediaController; -import android.media.session.MediaSession; -import android.util.Log; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageButton; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.SeekBar; -import android.widget.TextView; - -import androidx.constraintlayout.widget.ConstraintSet; -import com.android.settingslib.media.LocalMediaManager; -import com.android.internal.widget.MediaNotificationView; -import com.android.systemui.R; -import com.android.systemui.media.IlluminationDrawable; -import com.android.systemui.media.MediaControlPanel; -import com.android.systemui.media.SeekBarObserver; -import com.android.systemui.media.SeekBarViewModel; -import com.android.systemui.plugins.ActivityStarter; -import com.android.systemui.util.concurrency.DelayableExecutor; - -import java.util.concurrent.Executor; - -/** - * Single media player for carousel in QSPanel - */ -public class QSMediaPlayer extends MediaControlPanel { - - private static final String TAG = "QSMediaPlayer"; - - // Button IDs for QS controls - static final int[] QS_ACTION_IDS = { - R.id.action0, - R.id.action1, - R.id.action2, - R.id.action3, - R.id.action4 - }; - - private final QSPanel mParent; - private final Executor mForegroundExecutor; - private final DelayableExecutor mBackgroundExecutor; - private final SeekBarViewModel mSeekBarViewModel; - private final SeekBarObserver mSeekBarObserver; - private String mPackageName; - - /** - * Initialize quick shade version of player - * @param context - * @param parent - * @param routeManager Provides information about device - * @param foregroundExecutor - * @param backgroundExecutor - * @param activityStarter - */ - public QSMediaPlayer(Context context, ViewGroup parent, LocalMediaManager routeManager, - Executor foregroundExecutor, DelayableExecutor backgroundExecutor, - ActivityStarter activityStarter) { - super(context, parent, routeManager, R.layout.qs_media_panel, QS_ACTION_IDS, - foregroundExecutor, backgroundExecutor, activityStarter); - mParent = (QSPanel) parent; - mForegroundExecutor = foregroundExecutor; - mBackgroundExecutor = backgroundExecutor; - mSeekBarViewModel = new SeekBarViewModel(backgroundExecutor); - mSeekBarObserver = new SeekBarObserver(getView()); - // Can't use the viewAttachLifecycle of media player because remove/add is used to adjust - // priority of players. As soon as it is removed, the lifecycle will end and the seek bar - // will stop updating. So, use the lifecycle of the parent instead. - mSeekBarViewModel.getProgress().observe(viewAttachLifecycle(parent), mSeekBarObserver); - SeekBar bar = getView().findViewById(R.id.media_progress_bar); - bar.setOnSeekBarChangeListener(mSeekBarViewModel.getSeekBarListener()); - bar.setOnTouchListener(mSeekBarViewModel.getSeekBarTouchListener()); - } - - /** - * Add a media panel view based on a media description. Used for resumption - * @param description - * @param iconColor - * @param bgColor - * @param contentIntent - * @param pkgName - */ - public void setMediaSession(MediaSession.Token token, MediaDescription description, - int iconColor, int bgColor, PendingIntent contentIntent, String pkgName) { - mPackageName = pkgName; - PackageManager pm = getContext().getPackageManager(); - Drawable icon = null; - CharSequence appName = pkgName.substring(pkgName.lastIndexOf(".")); - try { - icon = pm.getApplicationIcon(pkgName); - appName = pm.getApplicationLabel(pm.getApplicationInfo(pkgName, 0)); - } catch (PackageManager.NameNotFoundException e) { - Log.e(TAG, "Error getting package information", e); - } - - // Set what we can normally - super.setMediaSession(token, icon, null, iconColor, bgColor, contentIntent, - appName.toString(), null); - - // Then add info from MediaDescription - ImageView albumView = mMediaNotifView.findViewById(R.id.album_art); - if (albumView != null) { - // Resize art in a background thread - mBackgroundExecutor.execute(() -> processAlbumArt(description, albumView)); - } - - // Song name - TextView titleText = mMediaNotifView.findViewById(R.id.header_title); - CharSequence songName = description.getTitle(); - titleText.setText(songName); - titleText.setTextColor(iconColor); - - // Artist name (not in mini player) - TextView artistText = mMediaNotifView.findViewById(R.id.header_artist); - if (artistText != null) { - CharSequence artistName = description.getSubtitle(); - artistText.setText(artistName); - artistText.setTextColor(iconColor); - } - - initLongPressMenu(iconColor); - - // Set buttons to resume state - resetButtons(); - } - - /** - * Update media panel view for the given media session - * @param token token for this media session - * @param icon app notification icon - * @param largeIcon notification's largeIcon, used as a fallback for album art - * @param iconColor foreground color (for text, icons) - * @param bgColor background color - * @param actionsContainer a LinearLayout containing the media action buttons - * @param contentIntent Intent to send when user taps on player - * @param appName Application title - * @param key original notification's key - */ - public void setMediaSession(MediaSession.Token token, Drawable icon, Icon largeIcon, - int iconColor, int bgColor, View actionsContainer, PendingIntent contentIntent, - String appName, String key) { - - super.setMediaSession(token, icon, largeIcon, iconColor, bgColor, contentIntent, appName, - key); - ConstraintSet constraintSet = mMediaNotifView.getConstraintSet(R.id.expanded); - // Media controls - if (actionsContainer != null) { - LinearLayout parentActionsLayout = (LinearLayout) actionsContainer; - int i = 0; - for (; i < parentActionsLayout.getChildCount() && i < QS_ACTION_IDS.length; i++) { - final ImageButton thisBtn = mMediaNotifView.findViewById(QS_ACTION_IDS[i]); - ImageButton thatBtn = parentActionsLayout.findViewById(NOTIF_ACTION_IDS[i]); - if (thatBtn == null || thatBtn.getDrawable() == null - || thatBtn.getVisibility() != View.VISIBLE) { - constraintSet.setVisibility(QS_ACTION_IDS[i], ConstraintSet.GONE); - continue; - } - - if (mMediaNotifView.getBackground() instanceof IlluminationDrawable) { - ((IlluminationDrawable) mMediaNotifView.getBackground()) - .setupTouch(thisBtn, mMediaNotifView); - } - - Drawable thatIcon = thatBtn.getDrawable(); - thisBtn.setImageDrawable(thatIcon.mutate()); - constraintSet.setVisibility(QS_ACTION_IDS[i], ConstraintSet.VISIBLE); - thisBtn.setOnClickListener(v -> { - Log.d(TAG, "clicking on other button"); - thatBtn.performClick(); - }); - } - - // Hide any unused buttons - for (; i < QS_ACTION_IDS.length; i++) { - constraintSet.setVisibility(QS_ACTION_IDS[i], ConstraintSet.GONE); - } - } - - // Seek Bar - final MediaController controller = new MediaController(getContext(), token); - mBackgroundExecutor.execute( - () -> mSeekBarViewModel.updateController(controller, iconColor)); - - initLongPressMenu(iconColor); - } - - private void initLongPressMenu(int iconColor) { - // Set up long press menu - // TODO: b/156036025 bring back media guts - } - - @Override - protected void resetButtons() { - super.resetButtons(); - mSeekBarViewModel.clearController(); -// View guts = mMediaNotifView.findViewById(R.id.media_guts); - View options = mMediaNotifView.findViewById(R.id.qs_media_controls_options); - - mMediaNotifView.setOnLongClickListener(v -> { - // Replace player view with close/cancel view - // TODO: b/156036025 make this work - options.setVisibility(View.VISIBLE); - return true; // consumed click - }); - } - - /** - * Sets the listening state of the player. - * - * Should be set to true when the QS panel is open. Otherwise, false. This is a signal to avoid - * unnecessary work when the QS panel is closed. - * - * @param listening True when player should be active. Otherwise, false. - */ - public void setListening(boolean listening) { - mSeekBarViewModel.setListening(listening); - } - - @Override - public void removePlayer() { - Log.d(TAG, "removing player from parent: " + mParent); - // Ensure this happens on the main thread (could happen in QSMediaBrowser callback) - mForegroundExecutor.execute(() -> mParent.removeMediaPlayer(QSMediaPlayer.this)); - } - - @Override - public String getMediaPlayerPackage() { - if (getController() == null) { - return mPackageName; - } - return super.getMediaPlayerPackage(); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java index e8f6c9668e9b3..128eee83e8fee 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java @@ -32,23 +32,19 @@ import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Color; import android.graphics.drawable.Drawable; -import android.graphics.drawable.Icon; import android.media.MediaDescription; -import android.media.session.MediaSession; import android.metrics.LogMaker; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.os.UserHandle; import android.os.UserManager; -import android.service.notification.StatusBarNotification; import android.service.quicksettings.Tile; import android.util.AttributeSet; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.HorizontalScrollView; import android.widget.LinearLayout; import com.android.internal.logging.MetricsLogger; @@ -56,17 +52,13 @@ import com.android.internal.logging.UiEventLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.statusbar.NotificationVisibility; import com.android.settingslib.Utils; -import com.android.settingslib.bluetooth.LocalBluetoothManager; -import com.android.settingslib.media.InfoMediaManager; -import com.android.settingslib.media.LocalMediaManager; import com.android.systemui.Dependency; import com.android.systemui.Dumpable; import com.android.systemui.R; import com.android.systemui.broadcast.BroadcastDispatcher; -import com.android.systemui.dagger.qualifiers.Background; -import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; import com.android.systemui.media.MediaControlPanel; +import com.android.systemui.media.MediaHierarchyManager; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.qs.DetailAdapter; import com.android.systemui.plugins.qs.QSTile; @@ -77,20 +69,17 @@ import com.android.systemui.qs.external.CustomTile; import com.android.systemui.qs.logging.QSLogger; import com.android.systemui.settings.BrightnessController; import com.android.systemui.settings.ToggleSliderView; -import com.android.systemui.statusbar.notification.NotificationEntryListener; -import com.android.systemui.statusbar.notification.NotificationEntryManager; +import com.android.systemui.statusbar.NotificationMediaManager; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.policy.BrightnessMirrorController; import com.android.systemui.statusbar.policy.BrightnessMirrorController.BrightnessMirrorListener; import com.android.systemui.tuner.TunerService; import com.android.systemui.tuner.TunerService.Tunable; -import com.android.systemui.util.concurrency.DelayableExecutor; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collection; -import java.util.concurrent.Executor; import java.util.stream.Collectors; import javax.inject.Inject; @@ -108,21 +97,14 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne protected final Context mContext; protected final ArrayList mRecords = new ArrayList<>(); private final BroadcastDispatcher mBroadcastDispatcher; + protected final MediaHierarchyManager mMediaHiearchyManager; private String mCachedSpecs = ""; protected final View mBrightnessView; private final H mHandler = new H(); private final MetricsLogger mMetricsLogger = Dependency.get(MetricsLogger.class); private final QSTileRevealController mQsTileRevealController; - private final LinearLayout mMediaCarousel; - private final ArrayList mMediaPlayers = new ArrayList<>(); - private final LocalBluetoothManager mLocalBluetoothManager; - private final Executor mForegroundExecutor; - private final DelayableExecutor mBackgroundExecutor; - private boolean mUpdateCarousel = false; private ActivityStarter mActivityStarter; - private NotificationEntryManager mNotificationEntryManager; - protected boolean mExpanded; protected boolean mListening; @@ -158,15 +140,6 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne } }; - private final NotificationEntryListener mNotificationEntryListener = - new NotificationEntryListener() { - @Override - public void onEntryRemoved(NotificationEntry entry, NotificationVisibility visibility, - boolean removedByUser, int reason) { - checkToRemoveMediaNotification(entry); - } - }; - @Inject public QSPanel( @Named(VIEW_CONTEXT) Context context, @@ -174,23 +147,17 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne DumpManager dumpManager, BroadcastDispatcher broadcastDispatcher, QSLogger qsLogger, - @Main Executor foregroundExecutor, - @Background DelayableExecutor backgroundExecutor, - @Nullable LocalBluetoothManager localBluetoothManager, + MediaHierarchyManager mediaHierarchyManager, ActivityStarter activityStarter, - NotificationEntryManager entryManager, UiEventLogger uiEventLogger ) { super(context, attrs); + mMediaHiearchyManager = mediaHierarchyManager; mContext = context; mQSLogger = qsLogger; mDumpManager = dumpManager; - mForegroundExecutor = foregroundExecutor; - mBackgroundExecutor = backgroundExecutor; - mLocalBluetoothManager = localBluetoothManager; mBroadcastDispatcher = broadcastDispatcher; mActivityStarter = activityStarter; - mNotificationEntryManager = entryManager; mUiEventLogger = uiEventLogger; setOrientation(VERTICAL); @@ -210,158 +177,27 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne addDivider(); - // Add media carousel - if (useQsMediaPlayer(context)) { - HorizontalScrollView mediaScrollView = (HorizontalScrollView) LayoutInflater.from( - mContext).inflate(R.layout.media_carousel, this, false); - mMediaCarousel = mediaScrollView.findViewById(R.id.media_carousel); - addView(mediaScrollView, 0); - } else { - mMediaCarousel = null; - } - mFooter = new QSSecurityFooter(this, context); addView(mFooter.getView()); + // Add media carousel + if (useQsMediaPlayer(context)) { + addMediaHostView(); + } + updateResources(); mBrightnessController = new BrightnessController(getContext(), findViewById(R.id.brightness_slider), mBroadcastDispatcher); } - @Override - public void onVisibilityAggregated(boolean isVisible) { - super.onVisibilityAggregated(isVisible); - if (!isVisible && mUpdateCarousel) { - for (QSMediaPlayer player : mMediaPlayers) { - if (player.isPlaying()) { - LayoutParams lp = (LayoutParams) player.getView().getLayoutParams(); - mMediaCarousel.removeView(player.getView()); - mMediaCarousel.addView(player.getView(), 0, lp); - ((HorizontalScrollView) mMediaCarousel.getParent()).fullScroll(View.FOCUS_LEFT); - mUpdateCarousel = false; - break; - } - } - } - } - - /** - * Add or update a player for the associated media session - * @param token - * @param icon - * @param largeIcon - * @param iconColor - * @param bgColor - * @param actionsContainer - * @param notif - * @param key - */ - public void addMediaSession(MediaSession.Token token, Drawable icon, Icon largeIcon, - int iconColor, int bgColor, View actionsContainer, StatusBarNotification notif, - String key) { - if (!useQsMediaPlayer(mContext)) { - // Shouldn't happen, but just in case - Log.e(TAG, "Tried to add media session without player!"); - return; - } - if (token == null) { - Log.e(TAG, "Media session token was null!"); - return; - } - - String packageName = notif.getPackageName(); - QSMediaPlayer player = findMediaPlayer(packageName, token, key); - - int playerWidth = (int) getResources().getDimension(R.dimen.qs_media_width); - int padding = (int) getResources().getDimension(R.dimen.qs_media_padding); - LayoutParams lp = new LayoutParams(playerWidth, ViewGroup.LayoutParams.MATCH_PARENT); - lp.setMarginStart(padding); - lp.setMarginEnd(padding); - - if (player == null) { - Log.d(TAG, "creating new player for " + packageName); - // Set up listener for device changes - // TODO: integrate with MediaTransferManager? - InfoMediaManager imm = new InfoMediaManager(mContext, notif.getPackageName(), - notif.getNotification(), mLocalBluetoothManager); - LocalMediaManager routeManager = new LocalMediaManager(mContext, mLocalBluetoothManager, - imm, notif.getPackageName()); - - player = new QSMediaPlayer(mContext, this, routeManager, mForegroundExecutor, - mBackgroundExecutor, mActivityStarter); - player.setListening(mListening); - if (player.isPlaying()) { - mMediaCarousel.addView(player.getView(), 0, lp); // add in front - } else { - mMediaCarousel.addView(player.getView(), lp); // add at end - } - mMediaPlayers.add(player); - } else if (player.isPlaying()) { - mUpdateCarousel = true; - } - - Log.d(TAG, "setting player session"); - String appName = Notification.Builder.recoverBuilder(getContext(), notif.getNotification()) - .loadHeaderAppName(); - player.setMediaSession(token, icon, largeIcon, iconColor, bgColor, actionsContainer, - notif.getNotification().contentIntent, appName, key); - - if (mMediaPlayers.size() > 0) { - ((View) mMediaCarousel.getParent()).setVisibility(View.VISIBLE); - } - } - - /** - * Check for an existing media player using the given information - * @param packageName - * @param token - * @param key - * @return a player, or null if no match found - */ - private QSMediaPlayer findMediaPlayer(String packageName, MediaSession.Token token, - String key) { - for (QSMediaPlayer player : mMediaPlayers) { - if (player.getKey() == null || key == null) { - // No notification key = loaded via mediabrowser, so just match on package - if (packageName.equals(player.getMediaPlayerPackage())) { - Log.d(TAG, "Found matching resume player by package: " + packageName); - return player; - } - } else if (player.getMediaSessionToken().equals(token)) { - Log.d(TAG, "Found matching player by token " + packageName); - return player; - } else if (packageName.equals(player.getMediaPlayerPackage()) - && key.equals(player.getKey())) { - // Also match if it's the same package and notification key - Log.d(TAG, "Found matching player by package " + packageName + ", " + key); - return player; - } - } - return null; - } - - protected View getMediaPanel() { - return mMediaCarousel; - } - - /** - * Remove the media player from the carousel - * @param player Player to remove - * @return true if removed, false if player was not found - */ - protected boolean removeMediaPlayer(QSMediaPlayer player) { - // Remove from list - if (!mMediaPlayers.remove(player)) { - return false; - } - - // Check if we need to collapse the carousel now - mMediaCarousel.removeView(player.getView()); - if (mMediaPlayers.size() == 0) { - ((View) mMediaCarousel.getParent()).setVisibility(View.GONE); - } - return true; + protected void addMediaHostView() { + ViewGroup hostView = mMediaHiearchyManager.createMediaHost( + MediaHierarchyManager.LOCATION_QS); + addView(hostView); + ViewGroup.LayoutParams layoutParams = hostView.getLayoutParams(); + layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT; + layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT; } private final QSMediaBrowser.Callback mMediaBrowserCallback = new QSMediaBrowser.Callback() { @@ -441,27 +277,6 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne mHasLoadedMediaControls = true; } - private void checkToRemoveMediaNotification(NotificationEntry entry) { - if (!useQsMediaPlayer(mContext)) { - return; - } - - if (!entry.isMediaNotification()) { - return; - } - - // If this entry corresponds to an existing set of controls, clear the controls - // This will handle apps that use an action to clear their notification - for (QSMediaPlayer p : mMediaPlayers) { - if (p.getKey() != null && p.getKey().equals(entry.getKey())) { - Log.d(TAG, "Clearing controls since notification removed " + entry.getKey()); - p.clearControls(); - return; - } - } - Log.d(TAG, "Media notification removed but no player found " + entry.getKey()); - } - protected void addDivider() { mDivider = LayoutInflater.from(mContext).inflate(R.layout.qs_divider, this, false); mDivider.setBackgroundColor(Utils.applyAlpha(mDivider.getAlpha(), @@ -528,7 +343,6 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne loadMediaResumptionControls(); } } - mNotificationEntryManager.addNotificationEntryListener(mNotificationEntryListener); } @Override @@ -545,7 +359,6 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne } mDumpManager.unregisterDumpable(getDumpableTag()); mBroadcastDispatcher.unregisterReceiver(mUserChangeReceiver); - mNotificationEntryManager.removeNotificationEntryListener(mNotificationEntryListener); super.onDetachedFromWindow(); } @@ -716,7 +529,7 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne public void setListening(boolean listening) { if (mListening == listening) return; - mListening = listening; + mMediaHiearchyManager.setShouldListen(listening); if (mTileLayout != null) { mQSLogger.logAllTilesChangeListening(listening, getDumpableTag(), mCachedSpecs); mTileLayout.setListening(listening); @@ -724,9 +537,6 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne if (mListening) { refreshAllTiles(); } - for (QSMediaPlayer player : mMediaPlayers) { - player.setListening(mListening); - } } private String getTilesSpecs() { diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickQSMediaPlayer.java b/packages/SystemUI/src/com/android/systemui/qs/QuickQSMediaPlayer.java deleted file mode 100644 index 7b53eed64ae6f..0000000000000 --- a/packages/SystemUI/src/com/android/systemui/qs/QuickQSMediaPlayer.java +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright (C) 2019 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. - */ - -package com.android.systemui.qs; - -import android.app.PendingIntent; -import android.content.Context; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.Icon; -import android.media.session.MediaController; -import android.media.session.MediaSession; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageButton; -import android.widget.LinearLayout; - -import com.android.systemui.R; -import com.android.systemui.media.IlluminationDrawable; -import com.android.systemui.media.MediaControlPanel; -import com.android.systemui.plugins.ActivityStarter; - -import java.util.concurrent.Executor; - -/** - * QQS mini media player - */ -public class QuickQSMediaPlayer extends MediaControlPanel { - - private static final String TAG = "QQSMediaPlayer"; - - // Button IDs for QS controls - private static final int[] QQS_ACTION_IDS = {R.id.action0, R.id.action1, R.id.action2}; - - /** - * Initialize mini media player for QQS - * @param context - * @param parent - * @param foregroundExecutor - * @param backgroundExecutor - * @param activityStarter - */ - public QuickQSMediaPlayer(Context context, ViewGroup parent, Executor foregroundExecutor, - Executor backgroundExecutor, ActivityStarter activityStarter) { - super(context, parent, null, R.layout.qs_media_panel, QQS_ACTION_IDS, - foregroundExecutor, backgroundExecutor, activityStarter); - } - - /** - * Update media panel view for the given media session - * @param token token for this media session - * @param icon app notification icon - * @param largeIcon notification's largeIcon, used as a fallback for album art - * @param iconColor foreground color (for text, icons) - * @param bgColor background color - * @param actionsContainer a LinearLayout containing the media action buttons - * @param actionsToShow indices of which actions to display in the mini player - * (max 3: Notification.MediaStyle.MAX_MEDIA_BUTTONS_IN_COMPACT) - * @param contentIntent Intent to send when user taps on the view - * @param key original notification's key - */ - public void setMediaSession(MediaSession.Token token, Drawable icon, Icon largeIcon, - int iconColor, int bgColor, View actionsContainer, int[] actionsToShow, - PendingIntent contentIntent, String key) { - // Only update if this is a different session and currently playing - String oldPackage = ""; - if (getController() != null) { - oldPackage = getController().getPackageName(); - } - MediaController controller = new MediaController(getContext(), token); - MediaSession.Token currentToken = getMediaSessionToken(); - boolean samePlayer = currentToken != null - && currentToken.equals(token) - && oldPackage.equals(controller.getPackageName()); - if (getController() != null && !samePlayer && !isPlaying(controller)) { - return; - } - - super.setMediaSession(token, icon, largeIcon, iconColor, bgColor, contentIntent, null, key); - - LinearLayout parentActionsLayout = (LinearLayout) actionsContainer; - int i = 0; - if (actionsToShow != null) { - int maxButtons = Math.min(actionsToShow.length, parentActionsLayout.getChildCount()); - maxButtons = Math.min(maxButtons, QQS_ACTION_IDS.length); - for (; i < maxButtons; i++) { - ImageButton thisBtn = mMediaNotifView.findViewById(QQS_ACTION_IDS[i]); - int thatId = NOTIF_ACTION_IDS[actionsToShow[i]]; - ImageButton thatBtn = parentActionsLayout.findViewById(thatId); - if (thatBtn == null || thatBtn.getDrawable() == null - || thatBtn.getVisibility() != View.VISIBLE) { - thisBtn.setVisibility(View.GONE); - continue; - } - - if (mMediaNotifView.getBackground() instanceof IlluminationDrawable) { - ((IlluminationDrawable) mMediaNotifView.getBackground()) - .setupTouch(thisBtn, mMediaNotifView); - } - - Drawable thatIcon = thatBtn.getDrawable(); - thisBtn.setImageDrawable(thatIcon.mutate()); - thisBtn.setVisibility(View.VISIBLE); - thisBtn.setOnClickListener(v -> { - thatBtn.performClick(); - }); - } - } - - // Hide any unused buttons - for (; i < QQS_ACTION_IDS.length; i++) { - ImageButton thisBtn = mMediaNotifView.findViewById(QQS_ACTION_IDS[i]); - thisBtn.setVisibility(View.GONE); - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java index 6683a1ce4f4f0..8dfa22f115d7d 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java @@ -18,38 +18,36 @@ package com.android.systemui.qs; import static com.android.systemui.util.InjectionInflationController.VIEW_CONTEXT; -import android.annotation.Nullable; import android.content.Context; import android.content.res.Configuration; import android.graphics.Rect; import android.util.AttributeSet; import android.view.Gravity; import android.view.View; +import android.view.ViewGroup; import android.widget.LinearLayout; +import androidx.annotation.NonNull; import com.android.internal.logging.UiEventLogger; -import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.systemui.Dependency; import com.android.systemui.R; import com.android.systemui.broadcast.BroadcastDispatcher; -import com.android.systemui.dagger.qualifiers.Background; -import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; import com.android.systemui.plugins.ActivityStarter; +import com.android.systemui.media.MediaData; +import com.android.systemui.media.MediaHierarchyManager; import com.android.systemui.plugins.qs.QSTile; import com.android.systemui.plugins.qs.QSTile.SignalState; import com.android.systemui.plugins.qs.QSTile.State; import com.android.systemui.qs.customize.QSCustomizer; import com.android.systemui.qs.logging.QSLogger; -import com.android.systemui.statusbar.notification.NotificationEntryManager; +import com.android.systemui.statusbar.NotificationMediaManager; import com.android.systemui.tuner.TunerService; import com.android.systemui.tuner.TunerService.Tunable; import com.android.systemui.util.Utils; -import com.android.systemui.util.concurrency.DelayableExecutor; import java.util.ArrayList; import java.util.Collection; -import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; @@ -63,20 +61,34 @@ public class QuickQSPanel extends QSPanel { private static final String TAG = "QuickQSPanel"; // Start it at 6 so a non-zero value can be obtained statically. private static int sDefaultMaxTiles = 6; + private final NotificationMediaManager mNotificationMediaManager; private boolean mDisabledByPolicy; private int mMaxTiles; protected QSPanel mFullPanel; - private QuickQSMediaPlayer mMediaPlayer; /** Whether or not the QS media player feature is enabled. */ private boolean mUsingMediaPlayer; /** Whether or not the QuickQSPanel currently contains a media player. */ - private boolean mHasMediaPlayer; + private boolean mShowHorizontalTileLayout; private LinearLayout mHorizontalLinearLayout; // Only used with media - private QSTileLayout mMediaTileLayout; + private QSTileLayout mHorizontalTileLayout; private QSTileLayout mRegularTileLayout; + private int mLastOrientation = -1; + private ViewGroup mMediaHost; + private final NotificationMediaManager.MediaListener mMediaCallback + = new NotificationMediaManager.MediaListener() { + @Override + public void onMediaDataLoaded(@NonNull String key, @NonNull MediaData data) { + switchTileLayout(); + } + + @Override + public void onMediaDataRemoved(@NonNull String key) { + switchTileLayout(); + } + }; @Inject public QuickQSPanel( @@ -85,16 +97,14 @@ public class QuickQSPanel extends QSPanel { DumpManager dumpManager, BroadcastDispatcher broadcastDispatcher, QSLogger qsLogger, - @Main Executor foregroundExecutor, - @Background DelayableExecutor backgroundExecutor, - @Nullable LocalBluetoothManager localBluetoothManager, + MediaHierarchyManager mediaHierarchyManager, + NotificationMediaManager notificationMediaManager, ActivityStarter activityStarter, - NotificationEntryManager entryManager, UiEventLogger uiEventLogger ) { super(context, attrs, dumpManager, broadcastDispatcher, qsLogger, - foregroundExecutor, backgroundExecutor, localBluetoothManager, activityStarter, - entryManager, uiEventLogger); + mediaHierarchyManager, activityStarter, uiEventLogger); + mNotificationMediaManager = notificationMediaManager; if (mFooter != null) { removeView(mFooter.getView()); } @@ -107,23 +117,17 @@ public class QuickQSPanel extends QSPanel { mUsingMediaPlayer = Utils.useQsMediaPlayer(context); if (mUsingMediaPlayer) { + mNotificationMediaManager.addCallback(mMediaCallback); mHorizontalLinearLayout = new LinearLayout(mContext); mHorizontalLinearLayout.setOrientation(LinearLayout.HORIZONTAL); mHorizontalLinearLayout.setClipChildren(false); mHorizontalLinearLayout.setClipToPadding(false); - int marginSize = (int) mContext.getResources().getDimension(R.dimen.qqs_media_spacing); - mMediaPlayer = new QuickQSMediaPlayer(mContext, mHorizontalLinearLayout, - foregroundExecutor, backgroundExecutor, activityStarter); - LayoutParams lp2 = new LayoutParams(0, LayoutParams.MATCH_PARENT, 1); - lp2.setMarginEnd(marginSize); - lp2.setMarginStart(0); - mHorizontalLinearLayout.addView(mMediaPlayer.getView(), lp2); - mTileLayout = new DoubleLineTileLayout(context, mUiEventLogger); - mMediaTileLayout = mTileLayout; + mHorizontalTileLayout = mTileLayout; mRegularTileLayout = new HeaderTileLayout(context, mUiEventLogger); LayoutParams lp = new LayoutParams(0, LayoutParams.MATCH_PARENT, 1); + int marginSize = (int) mContext.getResources().getDimension(R.dimen.qqs_media_spacing); lp.setMarginEnd(0); lp.setMarginStart(marginSize); mHorizontalLinearLayout.addView((View) mTileLayout, lp); @@ -135,6 +139,7 @@ public class QuickQSPanel extends QSPanel { ((View) mRegularTileLayout).setVisibility(View.GONE); addView((View) mRegularTileLayout, 0); super.setPadding(0, 0, 0, 0); + reAttachMediaHost(); } else { sDefaultMaxTiles = getResources().getInteger(R.integer.quick_qs_panel_max_columns); mTileLayout = new HeaderTileLayout(context, mUiEventLogger); @@ -144,8 +149,24 @@ public class QuickQSPanel extends QSPanel { } } - public QuickQSMediaPlayer getMediaPlayer() { - return mMediaPlayer; + private void reAttachMediaHost() { + ViewGroup newParent = shouldUseHorizontalTileLayout() ? mHorizontalLinearLayout : this; + ViewGroup currentParent = (ViewGroup) mMediaHost.getParent(); + if (currentParent != newParent) { + if (currentParent != null) { + currentParent.removeView(mMediaHost); + } + newParent.addView(mMediaHost); + ViewGroup.LayoutParams layoutParams = mMediaHost.getLayoutParams(); + layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT; + layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT; + } + } + + @Override + protected void addMediaHostView() { + mMediaHost = mMediaHiearchyManager.createMediaHost( + MediaHierarchyManager.LOCATION_QQS); } @Override @@ -167,6 +188,7 @@ public class QuickQSPanel extends QSPanel { protected void onDetachedFromWindow() { super.onDetachedFromWindow(); Dependency.get(TunerService.class).removeTunable(mNumTiles); + mNotificationMediaManager.removeCallback(mMediaCallback); } @Override @@ -191,10 +213,19 @@ public class QuickQSPanel extends QSPanel { super.drawTile(r, state); } + @Override + protected void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + if (newConfig.orientation != mLastOrientation) { + mLastOrientation = newConfig.orientation; + switchTileLayout(); + } + } + boolean switchTileLayout() { if (!mUsingMediaPlayer) return false; - mHasMediaPlayer = mMediaPlayer.hasMediaSession(); - if (mHasMediaPlayer && mHorizontalLinearLayout.getVisibility() == View.GONE) { + mShowHorizontalTileLayout = shouldUseHorizontalTileLayout(); + if (mShowHorizontalTileLayout && mHorizontalLinearLayout.getVisibility() == View.GONE) { mHorizontalLinearLayout.setVisibility(View.VISIBLE); ((View) mRegularTileLayout).setVisibility(View.GONE); mTileLayout.setListening(false); @@ -202,11 +233,13 @@ public class QuickQSPanel extends QSPanel { mTileLayout.removeTile(record); record.tile.removeCallback(record.callback); } - mTileLayout = mMediaTileLayout; + mTileLayout = mHorizontalTileLayout; if (mHost != null) setTiles(mHost.getTiles()); mTileLayout.setListening(mListening); + reAttachMediaHost(); return true; - } else if (!mHasMediaPlayer && mHorizontalLinearLayout.getVisibility() == View.VISIBLE) { + } else if (!mShowHorizontalTileLayout + && mHorizontalLinearLayout.getVisibility() == View.VISIBLE) { mHorizontalLinearLayout.setVisibility(View.GONE); ((View) mRegularTileLayout).setVisibility(View.VISIBLE); mTileLayout.setListening(false); @@ -217,14 +250,21 @@ public class QuickQSPanel extends QSPanel { mTileLayout = mRegularTileLayout; if (mHost != null) setTiles(mHost.getTiles()); mTileLayout.setListening(mListening); + reAttachMediaHost(); return true; } return false; } - /** Returns true if this panel currently contains a media player. */ - public boolean hasMediaPlayer() { - return mHasMediaPlayer; + private boolean shouldUseHorizontalTileLayout() { + return mNotificationMediaManager.hasActiveMedia() + && getResources().getConfiguration().orientation + == Configuration.ORIENTATION_LANDSCAPE; + } + + /** Returns true if this panel currently uses a horizontal tile layout. */ + public boolean usesHorizontalLayout() { + return mShowHorizontalTileLayout; } @Override diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java index b15c6a3e3b59a..b14697d831ac0 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java @@ -341,7 +341,7 @@ public class QuickStatusBarHeader extends RelativeLayout implements if (mQsDisabled) { lp.height = resources.getDimensionPixelSize( com.android.internal.R.dimen.quick_qs_offset_height); - } else if (useQsMediaPlayer(mContext) && mHeaderQsPanel.hasMediaPlayer()) { + } else if (useQsMediaPlayer(mContext) && mHeaderQsPanel.usesHorizontalLayout()) { lp.height = Math.max(getMinimumHeight(), resources.getDimensionPixelSize( com.android.internal.R.dimen.quick_qs_total_height_with_media)); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java index 459c95b82ba48..63ba57ba12536 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java @@ -44,14 +44,17 @@ import android.util.Log; import android.view.View; import android.widget.ImageView; +import androidx.annotation.NonNull; + import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; import com.android.internal.statusbar.NotificationVisibility; -import com.android.keyguard.KeyguardMediaPlayer; import com.android.systemui.Dependency; import com.android.systemui.Dumpable; import com.android.systemui.Interpolators; import com.android.systemui.colorextraction.SysuiColorExtractor; import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.media.MediaData; +import com.android.systemui.media.MediaDataManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.dagger.StatusBarModule; import com.android.systemui.statusbar.notification.NotificationEntryListener; @@ -103,6 +106,7 @@ public class NotificationMediaManager implements Dumpable { } private final NotificationEntryManager mEntryManager; + private final MediaDataManager mMediaDataManager; @Nullable private Lazy mNotificationShadeWindowController; @@ -113,7 +117,6 @@ public class NotificationMediaManager implements Dumpable { private ScrimController mScrimController; @Nullable private LockscreenWallpaper mLockscreenWallpaper; - private final KeyguardMediaPlayer mMediaPlayer; private final Executor mMainExecutor; @@ -187,13 +190,12 @@ public class NotificationMediaManager implements Dumpable { NotificationEntryManager notificationEntryManager, MediaArtworkProcessor mediaArtworkProcessor, KeyguardBypassController keyguardBypassController, - KeyguardMediaPlayer keyguardMediaPlayer, @Main Executor mainExecutor, - DeviceConfigProxy deviceConfig) { + DeviceConfigProxy deviceConfig, + MediaDataManager mediaDataManager) { mContext = context; mMediaArtworkProcessor = mediaArtworkProcessor; mKeyguardBypassController = keyguardBypassController; - mMediaPlayer = keyguardMediaPlayer; mMediaListeners = new ArrayList<>(); // TODO: use MediaSessionManager.SessionListener to hook us up to future updates // in session state @@ -204,7 +206,37 @@ public class NotificationMediaManager implements Dumpable { mNotificationShadeWindowController = notificationShadeWindowController; mEntryManager = notificationEntryManager; mMainExecutor = mainExecutor; + mMediaDataManager = mediaDataManager; + mediaDataManager.setListener(new MediaListener() { + @Override + public void onMediaDataLoaded(@NonNull String key, @NonNull MediaData metadata) { + ArrayList callbacks = new ArrayList<>(mMediaListeners); + for (int i = 0; i < callbacks.size(); i++) { + callbacks.get(i).onMediaDataLoaded(key, metadata); + } + } + + @Override + public void onMediaDataRemoved(@NonNull String key) { + ArrayList callbacks = new ArrayList<>(mMediaListeners); + for (int i = 0; i < callbacks.size(); i++) { + callbacks.get(i).onMediaDataRemoved(key); + } + } + }); + notificationEntryManager.addNotificationEntryListener(new NotificationEntryListener() { + + @Override + public void onPendingEntryAdded(NotificationEntry entry) { + mMediaDataManager.onNotificationAdded(entry.getKey(), entry.getSbn()); + } + + @Override + public void onPreEntryUpdated(NotificationEntry entry) { + mMediaDataManager.onNotificationAdded(entry.getKey(), entry.getSbn()); + } + @Override public void onEntryInflated(NotificationEntry entry) { findAndUpdateMediaNotifications(); @@ -222,6 +254,7 @@ public class NotificationMediaManager implements Dumpable { boolean removedByUser, int reason) { onNotificationRemoved(entry.getKey()); + mMediaDataManager.onNotificationRemoved(entry.getKey()); } }); @@ -278,7 +311,7 @@ public class NotificationMediaManager implements Dumpable { public void addCallback(MediaListener callback) { mMediaListeners.add(callback); - callback.onMetadataOrStateChanged(mMediaMetadata, + callback.onPrimaryMetadataOrStateChanged(mMediaMetadata, getMediaControllerPlaybackState(mMediaController)); } @@ -392,7 +425,7 @@ public class NotificationMediaManager implements Dumpable { @PlaybackState.State int state = getMediaControllerPlaybackState(mMediaController); ArrayList callbacks = new ArrayList<>(mMediaListeners); for (int i = 0; i < callbacks.size(); i++) { - callbacks.get(i).onMetadataOrStateChanged(mMediaMetadata, state); + callbacks.get(i).onPrimaryMetadataOrStateChanged(mMediaMetadata, state); } } @@ -473,7 +506,6 @@ public class NotificationMediaManager implements Dumpable { && mBiometricUnlockController.isWakeAndUnlock(); if (mKeyguardStateController.isLaunchTransitionFadingAway() || wakeAndUnlock) { mBackdrop.setVisibility(View.INVISIBLE); - mMediaPlayer.clearControls(); Trace.endSection(); return; } @@ -496,14 +528,6 @@ public class NotificationMediaManager implements Dumpable { } } - NotificationEntry entry = mEntryManager - .getActiveNotificationUnfiltered(mMediaNotificationKey); - if (entry != null) { - mMediaPlayer.updateControls(entry, getMediaIcon(), mediaMetadata); - } else { - mMediaPlayer.clearControls(); - } - // Process artwork on a background thread and send the resulting bitmap to // finishUpdateMediaMetaData. if (metaDataChanged) { @@ -626,7 +650,6 @@ public class NotificationMediaManager implements Dumpable { // We are unlocking directly - no animation! mBackdrop.setVisibility(View.GONE); mBackdropBack.setImageDrawable(null); - mMediaPlayer.clearControls(); if (windowController != null) { windowController.setBackdropShowing(false); } @@ -643,7 +666,6 @@ public class NotificationMediaManager implements Dumpable { mBackdrop.setVisibility(View.GONE); mBackdropFront.animate().cancel(); mBackdropBack.setImageDrawable(null); - mMediaPlayer.clearControls(); mMainExecutor.execute(mHideBackdropFront); }); if (mKeyguardStateController.isKeyguardFadingAway()) { @@ -697,6 +719,13 @@ public class NotificationMediaManager implements Dumpable { mProcessArtworkTasks.remove(task); } + /** + * Are there any media notifications active? + */ + public boolean hasActiveMedia() { + return mMediaDataManager.hasActiveMedia(); + } + /** * {@link AsyncTask} to prepare album art for use as backdrop on lock screen. */ @@ -750,6 +779,17 @@ public class NotificationMediaManager implements Dumpable { * @param state Current playback state * @see PlaybackState.State */ - void onMetadataOrStateChanged(MediaMetadata metadata, @PlaybackState.State int state); + default void onPrimaryMetadataOrStateChanged(MediaMetadata metadata, + @PlaybackState.State int state) {} + + /** + * Called whenever there's new MediaData Loaded for the consumption in views + */ + default void onMediaDataLoaded(@NonNull String key, @NonNull MediaData data) {} + + /** + * Called whenever a previously existing Media notification was removed + */ + default void onMediaDataRemoved(@NonNull String key) {} } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java index de7e36d97b228..f0fed13114ba8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java @@ -21,9 +21,9 @@ import android.content.Context; import android.os.Handler; import com.android.internal.statusbar.IStatusBarService; -import com.android.keyguard.KeyguardMediaPlayer; import com.android.systemui.bubbles.BubbleController; import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.media.MediaDataManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.MediaArtworkProcessor; @@ -95,9 +95,9 @@ public interface StatusBarDependenciesModule { NotificationEntryManager notificationEntryManager, MediaArtworkProcessor mediaArtworkProcessor, KeyguardBypassController keyguardBypassController, - KeyguardMediaPlayer keyguardMediaPlayer, @Main Executor mainExecutor, - DeviceConfigProxy deviceConfigProxy) { + DeviceConfigProxy deviceConfigProxy, + MediaDataManager mediaDataManager) { return new NotificationMediaManager( context, statusBarLazy, @@ -105,9 +105,9 @@ public interface StatusBarDependenciesModule { notificationEntryManager, mediaArtworkProcessor, keyguardBypassController, - keyguardMediaPlayer, mainExecutor, - deviceConfigProxy); + deviceConfigProxy, + mediaDataManager); } /** */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java index cb0c2838c24df..634872d9d761d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java @@ -209,7 +209,7 @@ public final class NotificationEntry extends ListEntry { } /** The key for this notification. Guaranteed to be immutable and unique */ - public String getKey() { + @NonNull public String getKey() { return mKey; } @@ -217,7 +217,7 @@ public final class NotificationEntry extends ListEntry { * The StatusBarNotification that represents one half of a NotificationEntry (the other half * being the Ranking). This object is swapped out whenever a notification is updated. */ - public StatusBarNotification getSbn() { + @NonNull public StatusBarNotification getSbn() { return mSbn; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionListener.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionListener.java index 59f119e987b4a..3fab6f7c38573 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionListener.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionListener.java @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.notification.collection.notifcollection; +import android.annotation.NonNull; import android.service.notification.NotificationListenerService; import android.service.notification.StatusBarNotification; @@ -43,13 +44,13 @@ public interface NotifCollectionListener { * there is no guarantee of order and they may not have had a chance to initialize yet. Instead, * use {@link #onEntryAdded} which is called after all initialization. */ - default void onEntryInit(NotificationEntry entry) { + default void onEntryInit(@NonNull NotificationEntry entry) { } /** * Called whenever a notification with a new key is posted. */ - default void onEntryAdded(NotificationEntry entry) { + default void onEntryAdded(@NonNull NotificationEntry entry) { } /** @@ -64,7 +65,7 @@ public interface NotifCollectionListener { * immediately after a user dismisses a notification: we wait until we receive confirmation from * system server before considering the notification removed. */ - default void onEntryRemoved(NotificationEntry entry, @CancellationReason int reason) { + default void onEntryRemoved(@NonNull NotificationEntry entry, @CancellationReason int reason) { } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/BypassHeadsUpNotifier.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/BypassHeadsUpNotifier.kt index 88888d10e283e..0fd865b603f81 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/BypassHeadsUpNotifier.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/BypassHeadsUpNotifier.kt @@ -75,7 +75,7 @@ class BypassHeadsUpNotifier @Inject constructor( mediaManager.addCallback(this) } - override fun onMetadataOrStateChanged(metadata: MediaMetadata?, state: Int) { + override fun onPrimaryMetadataOrStateChanged(metadata: MediaMetadata?, state: Int) { val previous = currentMediaEntry var newEntry = entryManager .getActiveNotificationUnfiltered(mediaManager.mediaNotificationKey) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMediaTemplateViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMediaTemplateViewWrapper.java index b96cff830f315..93d3f3bdbe96d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMediaTemplateViewWrapper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMediaTemplateViewWrapper.java @@ -178,38 +178,6 @@ public class NotificationMediaTemplateViewWrapper extends NotificationTemplateVi final MediaSession.Token token = mRow.getEntry().getSbn().getNotification().extras .getParcelable(Notification.EXTRA_MEDIA_SESSION); - if (Utils.useQsMediaPlayer(mContext) && token != null) { - final int[] compactActions = mRow.getEntry().getSbn().getNotification().extras - .getIntArray(Notification.EXTRA_COMPACT_ACTIONS); - int tintColor = getNotificationHeader().getOriginalIconColor(); - NotificationShadeWindowController ctrl = Dependency.get( - NotificationShadeWindowController.class); - QuickQSPanel panel = ctrl.getNotificationShadeView().findViewById( - com.android.systemui.R.id.quick_qs_panel); - StatusBarNotification sbn = mRow.getEntry().getSbn(); - Notification notif = sbn.getNotification(); - Drawable iconDrawable = notif.getSmallIcon().loadDrawable(mContext); - panel.getMediaPlayer().setMediaSession(token, - iconDrawable, - notif.getLargeIcon(), - tintColor, - mBackgroundColor, - mActions, - compactActions, - notif.contentIntent, - sbn.getKey()); - QSPanel bigPanel = ctrl.getNotificationShadeView().findViewById( - com.android.systemui.R.id.quick_settings_panel); - bigPanel.addMediaSession(token, - iconDrawable, - notif.getLargeIcon(), - tintColor, - mBackgroundColor, - mActions, - sbn, - sbn.getKey()); - } - boolean showCompactSeekbar = mMediaManager.getShowCompactMediaSeekbar(); if (token == null || (COMPACT_MEDIA_TAG.equals(mView.getTag()) && !showCompactSeekbar)) { if (mSeekBarView != null) { 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 ab055e1bdc36f..82560f55509ae 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 @@ -19,6 +19,7 @@ package com.android.systemui.statusbar.notification.stack; import android.content.Context; import android.util.AttributeSet; import android.view.View; +import android.view.ViewGroup; import com.android.systemui.R; import com.android.systemui.statusbar.notification.row.ActivatableNotificationView; @@ -37,7 +38,6 @@ public class MediaHeaderView extends ActivatableNotificationView { @Override protected void onFinishInflate() { super.onFinishInflate(); - mContentView = findViewById(R.id.keyguard_media_view); } @Override @@ -52,4 +52,12 @@ public class MediaHeaderView extends ActivatableNotificationView { public void setBackgroundColor(int color) { setTintColor(color); } + + public void setMediaHost(ViewGroup mediaHost) { + mContentView = mediaHost; + addView(mediaHost); + ViewGroup.LayoutParams layoutParams = mediaHost.getLayoutParams(); + layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT; + layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT; + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.java index 6eec1ca33e145..1d01db453da49 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.java @@ -29,10 +29,12 @@ import android.content.Intent; import android.provider.Settings; import android.view.LayoutInflater; import android.view.View; +import android.view.ViewGroup; import com.android.internal.annotations.VisibleForTesting; -import com.android.keyguard.KeyguardMediaPlayer; import com.android.systemui.R; +import com.android.systemui.media.KeyguardMediaController; +import com.android.systemui.media.MediaHierarchyManager; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.StatusBarState; @@ -74,8 +76,8 @@ public class NotificationSectionsManager implements StackScrollAlgorithm.Section private final StatusBarStateController mStatusBarStateController; private final ConfigurationController mConfigurationController; private final PeopleHubViewAdapter mPeopleHubViewAdapter; - private final KeyguardMediaPlayer mKeyguardMediaPlayer; private final NotificationSectionsFeatureManager mSectionsFeatureManager; + private final KeyguardMediaController mKeyguardMediaController; private final int mNumberOfSections; private final PeopleHubViewBoundary mPeopleHubViewBoundary = new PeopleHubViewBoundary() { @@ -123,15 +125,16 @@ public class NotificationSectionsManager implements StackScrollAlgorithm.Section StatusBarStateController statusBarStateController, ConfigurationController configurationController, PeopleHubViewAdapter peopleHubViewAdapter, - KeyguardMediaPlayer keyguardMediaPlayer, + MediaHierarchyManager mediaHiearchyManager, + KeyguardMediaController keyguardMediaController, NotificationSectionsFeatureManager sectionsFeatureManager) { mActivityStarter = activityStarter; mStatusBarStateController = statusBarStateController; mConfigurationController = configurationController; mPeopleHubViewAdapter = peopleHubViewAdapter; - mKeyguardMediaPlayer = keyguardMediaPlayer; mSectionsFeatureManager = sectionsFeatureManager; mNumberOfSections = mSectionsFeatureManager.getNumberOfBuckets(); + mKeyguardMediaController = keyguardMediaController; } NotificationSection[] createSectionsForBuckets() { @@ -205,12 +208,9 @@ public class NotificationSectionsManager implements StackScrollAlgorithm.Section mIncomingHeader.setHeaderText(R.string.notification_section_header_incoming); mIncomingHeader.setOnHeaderClickListener(this::onGentleHeaderClick); - if (mMediaControlsView != null) { - mKeyguardMediaPlayer.unbindView(); - } mMediaControlsView = reinflateView(mMediaControlsView, layoutInflater, R.layout.keyguard_media_header); - mKeyguardMediaPlayer.bindView(mMediaControlsView); + mKeyguardMediaController.attach(mMediaControlsView); } /** Listener for when the "clear all" button is clicked on the gentle notification header. */ 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 c9716d39590e0..aa6baffbeba40 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java @@ -69,6 +69,7 @@ import com.android.systemui.dagger.qualifiers.DisplayId; import com.android.systemui.doze.DozeLog; import com.android.systemui.fragments.FragmentHostManager; import com.android.systemui.fragments.FragmentHostManager.FragmentListener; +import com.android.systemui.media.MediaHierarchyManager; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.qs.QS; import com.android.systemui.plugins.statusbar.StatusBarStateController; @@ -242,6 +243,7 @@ public class NotificationPanelViewController extends PanelViewController { private final KeyguardBypassController mKeyguardBypassController; private final KeyguardUpdateMonitor mUpdateMonitor; private final ConversationNotificationManager mConversationNotificationManager; + private final MediaHierarchyManager mMediaHierarchyManager; private KeyguardAffordanceHelper mAffordanceHelper; private KeyguardUserSwitcher mKeyguardUserSwitcher; @@ -456,7 +458,8 @@ public class NotificationPanelViewController extends PanelViewController { ConfigurationController configurationController, FlingAnimationUtils.Builder flingAnimationUtilsBuilder, StatusBarTouchableRegionManager statusBarTouchableRegionManager, - ConversationNotificationManager conversationNotificationManager) { + ConversationNotificationManager conversationNotificationManager, + MediaHierarchyManager mediaHierarchyManager) { super(view, falsingManager, dozeLog, keyguardStateController, (SysuiStatusBarStateController) statusBarStateController, vibratorHelper, latencyTracker, flingAnimationUtilsBuilder, statusBarTouchableRegionManager); @@ -466,6 +469,7 @@ public class NotificationPanelViewController extends PanelViewController { mZenModeController = zenModeController; mConfigurationController = configurationController; mFlingAnimationUtilsBuilder = flingAnimationUtilsBuilder; + mMediaHierarchyManager = mediaHierarchyManager; mView.setWillNotDraw(!DEBUG); mInjectionInflationController = injectionInflationController; mFalsingManager = falsingManager; @@ -1609,7 +1613,7 @@ public class NotificationPanelViewController extends PanelViewController { if (mQs == null) return; float qsExpansionFraction = getQsExpansionFraction(); mQs.setQsExpansion(qsExpansionFraction, getHeaderTranslation()); - int heightDiff = mQs.getDesiredHeight() - mQs.getQsMinExpansionHeight(); + mMediaHierarchyManager.setQsExpansion(qsExpansionFraction); mNotificationStackScroller.setQsExpansionFraction(qsExpansionFraction); } diff --git a/packages/SystemUI/src/com/android/systemui/util/animation/UniqueObjectHost.kt b/packages/SystemUI/src/com/android/systemui/util/animation/UniqueObjectHost.kt new file mode 100644 index 0000000000000..63fc6c2481fbc --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/animation/UniqueObjectHost.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2020 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 + */ + +package com.android.systemui.util.animation + +import android.content.Context +import android.widget.FrameLayout + +/** + * A special view that hosts a unique object, which only exists once, but can transition between + * different hosts. If the view currently hosts the unique object, it's measuring it normally, + * but if it's not attached, it will obtain the size by requesting a measure. + */ +class UniqueObjectHost(context: Context) : FrameLayout(context) { + private var cachedHeight: Int = 0 + private var cachedWidth: Int = 0 + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + if (isCurrentHost()) { + cachedHeight = measuredHeight + cachedWidth = measuredWidth + } else { + setMeasuredDimension(cachedWidth, cachedHeight) + } + } + + private fun isCurrentHost() = childCount != 0 +} diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMediaPlayerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMediaPlayerTest.kt deleted file mode 100644 index 4bcf917fa95d4..0000000000000 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMediaPlayerTest.kt +++ /dev/null @@ -1,176 +0,0 @@ -/* - * Copyright (C) 2020 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. - */ - -package com.android.keyguard - -import android.app.Notification -import android.graphics.drawable.Icon -import android.media.MediaMetadata -import android.media.session.MediaController -import android.media.session.MediaSession -import android.media.session.PlaybackState -import android.testing.AndroidTestingRunner -import android.testing.TestableLooper -import android.view.View -import android.widget.TextView -import androidx.arch.core.executor.ArchTaskExecutor -import androidx.arch.core.executor.TaskExecutor -import androidx.test.filters.SmallTest - -import com.android.systemui.R -import com.android.systemui.SysuiTestCase -import com.android.systemui.statusbar.notification.collection.NotificationEntry -import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder -import com.android.systemui.media.MediaControllerFactory -import com.android.systemui.util.concurrency.FakeExecutor -import com.android.systemui.util.time.FakeSystemClock -import com.google.common.truth.Truth.assertThat - -import org.junit.After -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.Mock -import org.mockito.Mockito.any -import org.mockito.Mockito.mock -import org.mockito.Mockito.verify -import org.mockito.Mockito.`when` as whenever - -@SmallTest -@RunWith(AndroidTestingRunner::class) -@TestableLooper.RunWithLooper -public class KeyguardMediaPlayerTest : SysuiTestCase() { - - private lateinit var keyguardMediaPlayer: KeyguardMediaPlayer - @Mock private lateinit var mockMediaFactory: MediaControllerFactory - @Mock private lateinit var mockMediaController: MediaController - private lateinit var playbackState: PlaybackState - private lateinit var fakeExecutor: FakeExecutor - private lateinit var mediaMetadata: MediaMetadata.Builder - private lateinit var entry: NotificationEntry - @Mock private lateinit var mockView: View - private lateinit var songView: TextView - private lateinit var artistView: TextView - @Mock private lateinit var mockIcon: Icon - - private val taskExecutor: TaskExecutor = object : TaskExecutor() { - public override fun executeOnDiskIO(runnable: Runnable) { - runnable.run() - } - public override fun postToMainThread(runnable: Runnable) { - runnable.run() - } - public override fun isMainThread(): Boolean { - return true - } - } - - @Before - public fun setup() { - playbackState = PlaybackState.Builder().run { - build() - } - mockMediaController = mock(MediaController::class.java) - whenever(mockMediaController.getPlaybackState()).thenReturn(playbackState) - mockMediaFactory = mock(MediaControllerFactory::class.java) - whenever(mockMediaFactory.create(any())).thenReturn(mockMediaController) - - fakeExecutor = FakeExecutor(FakeSystemClock()) - keyguardMediaPlayer = KeyguardMediaPlayer(context, mockMediaFactory, fakeExecutor) - mockIcon = mock(Icon::class.java) - - mockView = mock(View::class.java) - songView = TextView(context) - artistView = TextView(context) - whenever(mockView.findViewById(R.id.header_title)).thenReturn(songView) - whenever(mockView.findViewById(R.id.header_artist)).thenReturn(artistView) - - mediaMetadata = MediaMetadata.Builder() - entry = NotificationEntryBuilder().build() - entry.getSbn().getNotification().extras.putParcelable(Notification.EXTRA_MEDIA_SESSION, - MediaSession.Token(1, null)) - - ArchTaskExecutor.getInstance().setDelegate(taskExecutor) - - keyguardMediaPlayer.bindView(mockView) - } - - @After - public fun tearDown() { - keyguardMediaPlayer.unbindView() - ArchTaskExecutor.getInstance().setDelegate(null) - } - - @Test - public fun testBind() { - keyguardMediaPlayer.unbindView() - keyguardMediaPlayer.bindView(mockView) - } - - @Test - public fun testUnboundClearControls() { - keyguardMediaPlayer.unbindView() - keyguardMediaPlayer.clearControls() - keyguardMediaPlayer.bindView(mockView) - } - - @Test - public fun testUpdateControls() { - keyguardMediaPlayer.updateControls(entry, mockIcon, mediaMetadata.build()) - FakeExecutor.exhaustExecutors(fakeExecutor) - verify(mockView).setVisibility(View.VISIBLE) - } - - @Test - public fun testClearControls() { - keyguardMediaPlayer.clearControls() - FakeExecutor.exhaustExecutors(fakeExecutor) - verify(mockView).setVisibility(View.GONE) - } - - @Test - public fun testUpdateControlsNullPlaybackState() { - // GIVEN that the playback state is null (ie. the media session was destroyed) - whenever(mockMediaController.getPlaybackState()).thenReturn(null) - // WHEN updated - keyguardMediaPlayer.updateControls(entry, mockIcon, mediaMetadata.build()) - FakeExecutor.exhaustExecutors(fakeExecutor) - // THEN the controls are cleared (ie. visibility is set to GONE) - verify(mockView).setVisibility(View.GONE) - } - - @Test - public fun testSongName() { - val song: String = "Song" - mediaMetadata.putText(MediaMetadata.METADATA_KEY_TITLE, song) - - keyguardMediaPlayer.updateControls(entry, mockIcon, mediaMetadata.build()) - - assertThat(fakeExecutor.runAllReady()).isEqualTo(1) - assertThat(songView.getText()).isEqualTo(song) - } - - @Test - public fun testArtistName() { - val artist: String = "Artist" - mediaMetadata.putText(MediaMetadata.METADATA_KEY_ARTIST, artist) - - keyguardMediaPlayer.updateControls(entry, mockIcon, mediaMetadata.build()) - - assertThat(fakeExecutor.runAllReady()).isEqualTo(1) - assertThat(artistView.getText()).isEqualTo(artist) - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardSliceProviderTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardSliceProviderTest.java index 92c1d76011063..f70fb4f55a8d3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardSliceProviderTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardSliceProviderTest.java @@ -131,7 +131,7 @@ public class KeyguardSliceProviderTest extends SysuiTestCase { MediaMetadata metadata = mock(MediaMetadata.class); when(metadata.getText(any())).thenReturn("metadata"); mProvider.onDozingChanged(true); - mProvider.onMetadataOrStateChanged(metadata, PlaybackState.STATE_PLAYING); + mProvider.onPrimaryMetadataOrStateChanged(metadata, PlaybackState.STATE_PLAYING); mProvider.onBindSlice(mProvider.getUri()); verify(metadata).getText(eq(MediaMetadata.METADATA_KEY_TITLE)); verify(metadata).getText(eq(MediaMetadata.METADATA_KEY_ARTIST)); @@ -144,7 +144,7 @@ public class KeyguardSliceProviderTest extends SysuiTestCase { when(metadata.getText(any())).thenReturn("metadata"); when(mKeyguardBypassController.getBypassEnabled()).thenReturn(true); when(mDozeParameters.getAlwaysOn()).thenReturn(true); - mProvider.onMetadataOrStateChanged(metadata, PlaybackState.STATE_PLAYING); + mProvider.onPrimaryMetadataOrStateChanged(metadata, PlaybackState.STATE_PLAYING); mProvider.onBindSlice(mProvider.getUri()); verify(metadata).getText(eq(MediaMetadata.METADATA_KEY_TITLE)); verify(metadata).getText(eq(MediaMetadata.METADATA_KEY_ARTIST)); @@ -210,7 +210,8 @@ public class KeyguardSliceProviderTest extends SysuiTestCase { mProvider.onStateChanged(StatusBarState.KEYGUARD); mProvider.onDozingChanged(true); reset(mContentResolver); - mProvider.onMetadataOrStateChanged(mock(MediaMetadata.class), PlaybackState.STATE_PLAYING); + mProvider.onPrimaryMetadataOrStateChanged(mock(MediaMetadata.class), + PlaybackState.STATE_PLAYING); verify(mContentResolver).notifyChange(eq(mProvider.getUri()), eq(null)); // Hides after waking up @@ -222,7 +223,8 @@ public class KeyguardSliceProviderTest extends SysuiTestCase { @Test public void onDozingChanged_updatesSliceIfMedia() { mProvider.onStateChanged(StatusBarState.KEYGUARD); - mProvider.onMetadataOrStateChanged(mock(MediaMetadata.class), PlaybackState.STATE_PLAYING); + mProvider.onPrimaryMetadataOrStateChanged(mock(MediaMetadata.class), + PlaybackState.STATE_PLAYING); reset(mContentResolver); // Show media when dozing mProvider.onDozingChanged(true); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java index 646bc9699ff84..cdf2b6faf98c6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java @@ -42,9 +42,9 @@ import android.view.ViewGroup; import androidx.test.filters.SmallTest; -import com.android.keyguard.KeyguardMediaPlayer; import com.android.systemui.ActivityStarterDelegate; import com.android.systemui.SysuiTestCase; +import com.android.systemui.media.MediaHierarchyManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.notification.NotificationSectionsFeatureManager; @@ -53,6 +53,7 @@ import com.android.systemui.statusbar.notification.row.ActivatableNotificationVi import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.dagger.NotificationRowComponent; import com.android.systemui.statusbar.policy.ConfigurationController; +import com.android.systemui.util.animation.UniqueObjectHost; import org.junit.Before; import org.junit.Rule; @@ -74,7 +75,7 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { @Mock private StatusBarStateController mStatusBarStateController; @Mock private ConfigurationController mConfigurationController; @Mock private PeopleHubViewAdapter mPeopleHubAdapter; - @Mock private KeyguardMediaPlayer mKeyguardMediaPlayer; + @Mock private MediaHierarchyManager mMediaHierarchyManager; @Mock private NotificationSectionsFeatureManager mSectionsFeatureManager; @Mock private NotificationRowComponent mNotificationRowComponent; @Mock private ActivatableNotificationViewController mActivatableNotificationViewController; @@ -87,13 +88,15 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { when(mNotificationRowComponent.getActivatableNotificationViewController()).thenReturn( mActivatableNotificationViewController ); + when(mMediaHierarchyManager.createMediaHost(any())).thenReturn( + new UniqueObjectHost(getContext())); mSectionsManager = new NotificationSectionsManager( mActivityStarterDelegate, mStatusBarStateController, mConfigurationController, mPeopleHubAdapter, - mKeyguardMediaPlayer, + mMediaHierarchyManager, mSectionsFeatureManager ); // Required in order for the header inflation to work properly diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewTest.java index 57ef05544e7ed..b5663d5dd19e1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewTest.java @@ -55,6 +55,7 @@ import com.android.systemui.R; import com.android.systemui.SysuiTestCase; import com.android.systemui.classifier.FalsingManagerFake; import com.android.systemui.doze.DozeLog; +import com.android.systemui.media.MediaHierarchyManager; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.FlingAnimationUtils; @@ -172,6 +173,8 @@ public class NotificationPanelViewTest extends SysuiTestCase { @Mock private ConfigurationController mConfigurationController; @Mock + private MediaHierarchyManager mMediaHiearchyManager; + @Mock private ConversationNotificationManager mConversationNotificationManager; private FlingAnimationUtils.Builder mFlingAnimationUtilsBuilder; @@ -228,7 +231,7 @@ public class NotificationPanelViewTest extends SysuiTestCase { mLatencyTracker, mPowerManager, mAccessibilityManager, 0, mUpdateMonitor, mMetricsLogger, mActivityManager, mZenModeController, mConfigurationController, mFlingAnimationUtilsBuilder, mStatusBarTouchableRegionManager, - mConversationNotificationManager); + mConversationNotificationManager, mMediaHiearchyManager); mNotificationPanelViewController.initDependencies(mStatusBar, mGroupManager, mNotificationShelf, mNotificationAreaController, mScrimController); mNotificationPanelViewController.setHeadsUpManager(mHeadsUpManager); From b52642b8d296ee38233a21718d84c448cc344b2a Mon Sep 17 00:00:00 2001 From: Selim Cinek Date: Fri, 17 Apr 2020 14:30:29 -0700 Subject: [PATCH 04/14] Changed the quick settings layout for media Previously media were side by side. Instead it's now below the QS Tiles. Bug: 154137987 Test: atest SystemUITests Change-Id: I5d88ae26ebdae257167982c8fe0cca208032f043 --- .../SystemUI/res/layout/media_carousel.xml | 2 +- .../SystemUI/res/layout/qqs_media_panel.xml | 90 -------------- .../SystemUI/res/layout/qs_footer_impl.xml | 10 -- packages/SystemUI/res/layout/qs_panel.xml | 22 +++- .../quick_status_bar_expanded_header.xml | 14 +-- packages/SystemUI/res/values/dimens.xml | 9 +- .../systemui/media/KeyguardMediaController.kt | 37 ++---- .../systemui/media/MediaDataManager.kt | 43 ++++++- .../systemui/media/MediaHierarchyManager.kt | 80 +++++++++---- .../com/android/systemui/media/MediaHost.kt | 69 +++++++++++ .../systemui/qs/DoubleLineTileLayout.kt | 2 +- .../android/systemui/qs/QSContainerImpl.java | 8 +- .../com/android/systemui/qs/QSFooterImpl.java | 3 - .../src/com/android/systemui/qs/QSPanel.java | 47 +++++--- .../com/android/systemui/qs/QuickQSPanel.java | 113 +++++++++++------- .../systemui/qs/QuickStatusBarHeader.java | 17 --- .../statusbar/NotificationMediaManager.java | 42 +------ .../stack/NotificationSectionsManager.java | 1 - .../com/android/systemui/qs/QSPanelTest.java | 10 +- .../NotificationSectionsManagerTest.java | 7 +- 20 files changed, 325 insertions(+), 301 deletions(-) delete mode 100644 packages/SystemUI/res/layout/qqs_media_panel.xml create mode 100644 packages/SystemUI/src/com/android/systemui/media/MediaHost.kt diff --git a/packages/SystemUI/res/layout/media_carousel.xml b/packages/SystemUI/res/layout/media_carousel.xml index f5fc67d6856c1..627cf88bc9ad4 100644 --- a/packages/SystemUI/res/layout/media_carousel.xml +++ b/packages/SystemUI/res/layout/media_carousel.xml @@ -20,8 +20,8 @@ xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" - android:padding="@dimen/qs_media_padding" android:scrollbars="none" + android:fillViewport="true" > - - - - - - - - - - - - - - - - - diff --git a/packages/SystemUI/res/layout/qs_footer_impl.xml b/packages/SystemUI/res/layout/qs_footer_impl.xml index 0c9ce3938420a..ebfd0a0fd5373 100644 --- a/packages/SystemUI/res/layout/qs_footer_impl.xml +++ b/packages/SystemUI/res/layout/qs_footer_impl.xml @@ -23,7 +23,6 @@ android:layout_height="@dimen/qs_footer_height" android:layout_marginStart="@dimen/qs_footer_margin" android:layout_marginEnd="@dimen/qs_footer_margin" - android:elevation="4dp" android:background="@android:color/transparent" android:baselineAligned="false" android:clickable="false" @@ -128,13 +127,4 @@ - - diff --git a/packages/SystemUI/res/layout/qs_panel.xml b/packages/SystemUI/res/layout/qs_panel.xml index 01dfeb281e1be..cdf84260e3998 100644 --- a/packages/SystemUI/res/layout/qs_panel.xml +++ b/packages/SystemUI/res/layout/qs_panel.xml @@ -54,20 +54,32 @@ android:layout_marginTop="@*android:dimen/quick_qs_offset_height" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginBottom="@dimen/qs_footer_height" android:elevation="4dp" android:background="@android:color/transparent" android:focusable="true" - android:accessibilityTraversalBefore="@android:id/edit" - /> + android:accessibilityTraversalBefore="@android:id/edit"> + + - - + + + + + diff --git a/packages/SystemUI/res/layout/quick_status_bar_expanded_header.xml b/packages/SystemUI/res/layout/quick_status_bar_expanded_header.xml index e99b91787072d..9a7c344baf201 100644 --- a/packages/SystemUI/res/layout/quick_status_bar_expanded_header.xml +++ b/packages/SystemUI/res/layout/quick_status_bar_expanded_header.xml @@ -20,7 +20,7 @@ xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/header" android:layout_width="match_parent" - android:layout_height="@*android:dimen/quick_qs_total_height" + android:layout_height="wrap_content" android:layout_gravity="@integer/notification_panel_layout_gravity" android:background="@android:color/transparent" android:baselineAligned="false" @@ -29,6 +29,7 @@ android:clipToPadding="false" android:paddingTop="0dp" android:paddingEnd="0dp" + android:paddingBottom="10dp" android:paddingStart="0dp" android:elevation="4dp" > @@ -45,8 +46,6 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_below="@id/quick_qs_status_icons" - android:layout_marginStart="@dimen/qs_header_tile_margin_horizontal" - android:layout_marginEnd="@dimen/qs_header_tile_margin_horizontal" android:accessibilityTraversalAfter="@+id/date_time_group" android:accessibilityTraversalBefore="@id/expand_indicator" android:clipChildren="false" @@ -54,15 +53,6 @@ android:focusable="true" android:importantForAccessibility="yes" /> - - 12dp 16dp 4dp + 18dp 16dp 8dp 24dp @@ -1042,6 +1043,10 @@ 48dp 8dp + + 16dp + 16dp + 4dp 0dp 0dp 24dp @@ -1229,12 +1234,12 @@ 350dp - 8dp + 16dp 16dp - 10dp 52dp 20dp 8dp + 8dp 5dp 5dp diff --git a/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt b/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt index f60c012c4c2c4..ae7860e8f2928 100644 --- a/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt @@ -17,8 +17,6 @@ package com.android.systemui.media import android.view.View -import com.android.internal.util.ContrastColorUtil -import com.android.systemui.statusbar.NotificationMediaManager import com.android.systemui.statusbar.notification.stack.MediaHeaderView import com.android.systemui.statusbar.phone.KeyguardBypassController import javax.inject.Inject @@ -30,37 +28,28 @@ import javax.inject.Singleton */ @Singleton class KeyguardMediaController @Inject constructor( - private val mediaHierarchyManager: MediaHierarchyManager, - private val notifMediaManager: NotificationMediaManager, + private val mediaHost: MediaHost, private val bypassController: KeyguardBypassController ) { private var view: MediaHeaderView? = null - init { - notifMediaManager.addCallback(object : NotificationMediaManager.MediaListener { - override fun onMediaDataLoaded(key: String, data: MediaData) { - updateVisibility() - } - - override fun onMediaDataRemoved(key: String) { - updateVisibility() - } - }) - } - /** * Attach this controller to a media view, initializing its state */ - fun attach(mediaControlsView: MediaHeaderView) { - view = mediaControlsView - val hostView = mediaHierarchyManager.createMediaHost( - MediaHierarchyManager.LOCATION_LOCKSCREEN) - mediaControlsView.setMediaHost(hostView) - updateVisibility() + fun attach(mediaView: MediaHeaderView) { + view = mediaView + mediaHost.visibleChangedListener = ::updateVisibility + mediaHost.isExpanded = false + mediaHost.showsOnlyActiveMedia = true + mediaHost.init(MediaHierarchyManager.LOCATION_LOCKSCREEN) + mediaView.setMediaHost(mediaHost.hostView) + /** + * Attach this controller to a media view, initializing its state + */ } - private fun updateVisibility() { - val shouldBeVisible = notifMediaManager.hasActiveMedia() && !bypassController.bypassEnabled + private fun updateVisibility(isVisible: Boolean) { + val shouldBeVisible = isVisible && !bypassController.bypassEnabled view?.visibility = if (shouldBeVisible) View.VISIBLE else View.GONE } } diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt index 298a5fd60813a..227e8e7b3986f 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt @@ -22,11 +22,13 @@ import android.graphics.Bitmap import android.graphics.drawable.Drawable import android.media.MediaMetadata import android.media.session.MediaSession +import android.media.session.PlaybackState import android.provider.Settings import android.service.notification.StatusBarNotification import androidx.core.graphics.drawable.RoundedBitmapDrawable import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory import com.android.internal.util.ContrastColorUtil +import com.android.settingslib.Utils import com.android.systemui.R import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main @@ -49,7 +51,7 @@ class MediaDataManager @Inject constructor( @Main private val foregroundExcecutor: Executor ) { - lateinit var listener: NotificationMediaManager.MediaListener + private val listeners: MutableSet = mutableSetOf() private var albumArtSize: Int = 0 private var albumArtRadius: Int = 0 private val mediaEntries: LinkedHashMap = LinkedHashMap() @@ -59,7 +61,8 @@ class MediaDataManager @Inject constructor( } private fun loadDimens() { - albumArtRadius = context.resources.getDimensionPixelSize(R.dimen.qs_media_corner_radius) + albumArtRadius = context.resources.getDimensionPixelSize( + Utils.getThemeAttr(context, android.R.attr.dialogCornerRadius)) albumArtSize = context.resources.getDimensionPixelSize(R.dimen.qs_media_album_size) } @@ -80,6 +83,16 @@ class MediaDataManager @Inject constructor( } } + /** + * Add a listener for changes in this class + */ + fun addListener(listener: Listener) = listeners.add(listener) + + /** + * Remove a listener for changes in this class + */ + fun removeListener(listener: Listener) = listeners.remove(listener) + private fun loadMediaDataInBg(key: String, sbn: StatusBarNotification) { val token = sbn.notification.extras.getParcelable(Notification.EXTRA_MEDIA_SESSION) as MediaSession.Token? @@ -164,14 +177,18 @@ class MediaDataManager @Inject constructor( if (mediaEntries.containsKey(key)) { // Otherwise this was removed already mediaEntries.put(key, data) - listener.onMediaDataLoaded(key, data) + listeners.forEach { + it.onMediaDataLoaded(key, data) + } } } fun onNotificationRemoved(key: String) { val removed = mediaEntries.remove(key) if (removed != null) { - listener.onMediaDataRemoved(key) + listeners.forEach { + it.onMediaDataRemoved(key) + } } } @@ -200,6 +217,24 @@ class MediaDataManager @Inject constructor( * Are there any media notifications active? */ fun hasActiveMedia() = mediaEntries.size > 0 + + fun hasAnyMedia(): Boolean { + // TODO: implement this when we implemented resumption + return hasActiveMedia() + } + + interface Listener { + + /** + * Called whenever there's new MediaData Loaded for the consumption in views + */ + fun onMediaDataLoaded(key: String, data: MediaData) {} + + /** + * Called whenever a previously existing Media notification was removed + */ + fun onMediaDataRemoved(key: String) {} + } } private val LOADING = MediaData(false, 0, 0, null, null, null, null, null, diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt index 4a650ab828bc8..c9630f4ff79c1 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt @@ -29,7 +29,6 @@ import com.android.systemui.R import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.plugins.statusbar.StatusBarStateController -import com.android.systemui.statusbar.NotificationMediaManager import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.notification.VisualStabilityManager import com.android.systemui.statusbar.phone.KeyguardBypassController @@ -52,12 +51,12 @@ class MediaHierarchyManager @Inject constructor( private val visualStabilityManager: VisualStabilityManager, private val statusBarStateController: StatusBarStateController, private val bypassController: KeyguardBypassController, - mediaManager: NotificationMediaManager + mediaManager: MediaDataManager ) { private val mediaCarousel: ViewGroup private val mediaContent: ViewGroup private val mediaPlayers: MutableMap = mutableMapOf() - private val mediaHosts = arrayOfNulls(LOCATION_LOCKSCREEN + 1) + private val mediaHosts = arrayOfNulls(LOCATION_LOCKSCREEN + 1) private val visualStabilityCallback = ::reorderAllPlayers private var currentAttachmentLocation = -1 @@ -71,8 +70,8 @@ class MediaHierarchyManager @Inject constructor( init { mediaCarousel = inflateMediaCarousel() - mediaContent = mediaCarousel.findViewById(R.id.media_carousel) - mediaManager.addCallback(object : NotificationMediaManager.MediaListener { + mediaContent = mediaCarousel.requireViewById(R.id.media_carousel) + mediaManager.addListener(object : MediaDataManager.Listener { override fun onMediaDataLoaded(key: String, data: MediaData) { updateView(key, data) } @@ -99,6 +98,7 @@ class MediaHierarchyManager @Inject constructor( mediaContent.addView(view, 0) } } + updateMediaPaddings() } private fun updateView(key: String, data: MediaData) { @@ -114,11 +114,8 @@ class MediaHierarchyManager @Inject constructor( existingPlayer = MediaControlPanel(context, mediaContent, routeManager, foregroundExecutor, backgroundExecutor) mediaPlayers[key] = existingPlayer - val padding = context.resources.getDimensionPixelSize(R.dimen.qs_media_padding) val lp = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) - lp.marginStart = padding - lp.marginEnd = padding existingPlayer.view.setLayoutParams(lp) existingPlayer.setListening(shouldListen) if (existingPlayer.isPlaying) { @@ -136,42 +133,83 @@ class MediaHierarchyManager @Inject constructor( } } existingPlayer.bind(data) + updateMediaPaddings() + } + + private fun updateMediaPaddings() { + val padding = context.resources.getDimensionPixelSize(R.dimen.qs_media_padding) + val childCount = mediaContent.childCount + for (i in 0 until childCount) { + val mediaView = mediaContent.getChildAt(i) + val desiredPaddingEnd = if (i == childCount - 1) 0 else padding + val layoutParams = mediaView.layoutParams as ViewGroup.MarginLayoutParams + if (layoutParams.marginEnd != desiredPaddingEnd) { + layoutParams.marginEnd = desiredPaddingEnd + mediaView.layoutParams = layoutParams + } + } + } /** - * Create a media host view which can be attached to a view hierarchy and where the player - * will be placed in when the host is the currently desired state. + * Register a media host and create a view can be attached to a view hierarchy + * and where the players will be placed in when the host is the currently desired state. * * @return the hostView associated with this location */ - fun createMediaHost(@MediaLocation location: Int) : ViewGroup { + fun register(mediaObject: MediaHost) : ViewGroup { val viewHost = UniqueObjectHost(context) - mediaHosts[location] = viewHost - if (location == currentAttachmentLocation) { + mediaObject.hostView = viewHost; + mediaHosts[mediaObject.location] = mediaObject + if (mediaObject.location == currentAttachmentLocation) { // 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 currentAttachmentLocation = -1 } - updateAttachmentLocation(animate = false) + updateAttachmentLocation() return viewHost } - private fun updateAttachmentLocation(animate: Boolean) { + private fun updateAttachmentLocation() { var desiredLocation = calculateLocation() if (desiredLocation != currentAttachmentLocation) { val host = mediaHosts[desiredLocation] host?.apply { - if (currentAttachmentLocation >= 0) { - val currentHost = mediaHosts[currentAttachmentLocation] - currentHost?.removeView(mediaCarousel) + // 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] } - host.addView(mediaCarousel) - // TODO: handle animation / repositioning etc + performTransition(previousHost, host) currentAttachmentLocation = desiredLocation } } } + 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 + } + } + @MediaLocation private fun calculateLocation() : Int { val onLockscreen = (!bypassController.bypassEnabled @@ -190,7 +228,7 @@ class MediaHierarchyManager @Inject constructor( var qsExpansion: Float = 0.0f set(value) { field = value - updateAttachmentLocation(animate = false) + updateAttachmentLocation() } @IntDef(prefix = ["LOCATION_"], value = [LOCATION_QS, LOCATION_QQS, LOCATION_LOCKSCREEN]) diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt new file mode 100644 index 0000000000000..7df61f2091f17 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt @@ -0,0 +1,69 @@ +package com.android.systemui.media + +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 mediaHierarchyManager: MediaHierarchyManager, + private val mediaDataManager: MediaDataManager +) { + 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 listener = object : MediaDataManager.Listener { + override fun onMediaDataLoaded(key: String, data: MediaData) { + updateViewVisibility() + } + + override fun onMediaDataRemoved(key: String) { + updateViewVisibility() + } + } + + /** + * Initialize this MediaObject and create a host view + */ + fun init(@MediaLocation location: Int) { + this.location = location; + hostView = mediaHierarchyManager.register(this) + hostView.addOnAttachStateChangeListener(object : OnAttachStateChangeListener { + override fun onViewAttachedToWindow(v: View?) { + mediaDataManager.addListener(listener) + updateViewVisibility() + } + + override fun onViewDetachedFromWindow(v: View?) { + mediaDataManager.removeListener(listener) + } + }) + updateViewVisibility() + } + + fun setShouldListen(listen: Boolean) { + // TODO: look into listening more + mediaHierarchyManager.shouldListen = listen + } + + private fun updateViewVisibility() { + if (showsOnlyActiveMedia) { + visible = mediaDataManager.hasActiveMedia() + } else { + visible = mediaDataManager.hasAnyMedia() + } + hostView.visibility = if (visible) View.VISIBLE else View.GONE + visibleChangedListener?.invoke(visible) + } +} \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/qs/DoubleLineTileLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/DoubleLineTileLayout.kt index c5ae3ab2c9fbf..40d317c7bb223 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/DoubleLineTileLayout.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/DoubleLineTileLayout.kt @@ -117,7 +117,7 @@ class DoubleLineTileLayout( it.tileView.measure(exactly(smallTileSize), exactly(smallTileSize)) } - val height = twoLineHeight + val height = twoLineHeight + paddingBottom + paddingTop setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), height) } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java index be8a8fd261508..088630c771d1c 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java @@ -42,7 +42,7 @@ public class QSContainerImpl extends FrameLayout { private QuickStatusBarHeader mHeader; private float mQsExpansion; private QSCustomizer mQSCustomizer; - private View mQSFooter; + private View mDragHandle; private View mBackground; private View mBackgroundGradient; @@ -62,7 +62,7 @@ public class QSContainerImpl extends FrameLayout { mQSDetail = findViewById(R.id.qs_detail); mHeader = findViewById(R.id.header); mQSCustomizer = findViewById(R.id.qs_customize); - mQSFooter = findViewById(R.id.qs_footer); + mDragHandle = findViewById(R.id.qs_drag_handle_view); mBackground = findViewById(R.id.quick_settings_background); mStatusBarBackground = findViewById(R.id.quick_settings_status_bar_background); mBackgroundGradient = findViewById(R.id.quick_settings_gradient_view); @@ -168,7 +168,7 @@ public class QSContainerImpl extends FrameLayout { setBottom(getTop() + height); mQSDetail.setBottom(getTop() + height); // Pin QS Footer to the bottom of the panel. - mQSFooter.setTranslationY(height - mQSFooter.getHeight()); + mDragHandle.setTranslationY(height - mDragHandle.getHeight()); mBackground.setTop(mQSPanel.getTop()); mBackground.setBottom(height); } @@ -192,13 +192,13 @@ public class QSContainerImpl extends FrameLayout { public void setExpansion(float expansion) { mQsExpansion = expansion; + mDragHandle.setAlpha(1.0f - expansion); updateExpansion(); } private void setMargins() { setMargins(mQSDetail); setMargins(mBackground); - setMargins(mQSFooter); mQSPanel.setMargins(mSideMargins); mHeader.setMargins(mSideMargins); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFooterImpl.java b/packages/SystemUI/src/com/android/systemui/qs/QSFooterImpl.java index 5de6d1c42b4ff..fc8e36ff22cf4 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSFooterImpl.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSFooterImpl.java @@ -98,7 +98,6 @@ public class QSFooterImpl extends FrameLayout implements QSFooter, private TouchAnimator mSettingsCogAnimator; private View mActionsContainer; - private View mDragHandle; private OnClickListener mExpandClickListener; @@ -146,7 +145,6 @@ public class QSFooterImpl extends FrameLayout implements QSFooter, mMultiUserSwitch = findViewById(R.id.multi_user_switch); mMultiUserAvatar = mMultiUserSwitch.findViewById(R.id.multi_user_avatar); - mDragHandle = findViewById(R.id.qs_drag_handle_view); mActionsContainer = findViewById(R.id.qs_footer_actions_container); mEditContainer = findViewById(R.id.qs_footer_actions_edit_container); @@ -219,7 +217,6 @@ public class QSFooterImpl extends FrameLayout implements QSFooter, return new TouchAnimator.Builder() .addFloat(mActionsContainer, "alpha", 0, 1) .addFloat(mEditContainer, "alpha", 0, 1) - .addFloat(mDragHandle, "alpha", 1, 0, 0) .addFloat(mPageIndicator, "alpha", 0, 1) .setStartDelay(0.15f) .build(); diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java index 128eee83e8fee..0222969efdaf8 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java @@ -59,6 +59,7 @@ import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dump.DumpManager; import com.android.systemui.media.MediaControlPanel; import com.android.systemui.media.MediaHierarchyManager; +import com.android.systemui.media.MediaHost; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.qs.DetailAdapter; import com.android.systemui.plugins.qs.QSTile; @@ -69,7 +70,6 @@ import com.android.systemui.qs.external.CustomTile; import com.android.systemui.qs.logging.QSLogger; import com.android.systemui.settings.BrightnessController; import com.android.systemui.settings.ToggleSliderView; -import com.android.systemui.statusbar.NotificationMediaManager; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.policy.BrightnessMirrorController; import com.android.systemui.statusbar.policy.BrightnessMirrorController.BrightnessMirrorListener; @@ -97,7 +97,7 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne protected final Context mContext; protected final ArrayList mRecords = new ArrayList<>(); private final BroadcastDispatcher mBroadcastDispatcher; - protected final MediaHierarchyManager mMediaHiearchyManager; + protected final MediaHost mMediaHost; private String mCachedSpecs = ""; protected final View mBrightnessView; private final H mHandler = new H(); @@ -147,12 +147,12 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne DumpManager dumpManager, BroadcastDispatcher broadcastDispatcher, QSLogger qsLogger, - MediaHierarchyManager mediaHierarchyManager, + MediaHost mediaHost, ActivityStarter activityStarter, UiEventLogger uiEventLogger ) { super(context, attrs); - mMediaHiearchyManager = mediaHierarchyManager; + mMediaHost = mediaHost; mContext = context; mQSLogger = qsLogger; mDumpManager = dumpManager; @@ -180,24 +180,39 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne mFooter = new QSSecurityFooter(this, context); addView(mFooter.getView()); - // Add media carousel - if (useQsMediaPlayer(context)) { - addMediaHostView(); - } - updateResources(); mBrightnessController = new BrightnessController(getContext(), findViewById(R.id.brightness_slider), mBroadcastDispatcher); } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + // Add media carousel at the end + if (useQsMediaPlayer(getContext())) { + addMediaHostView(); + } + } + protected void addMediaHostView() { - ViewGroup hostView = mMediaHiearchyManager.createMediaHost( - MediaHierarchyManager.LOCATION_QS); + mMediaHost.init(MediaHierarchyManager.LOCATION_QS); + mMediaHost.setExpanded(true); + mMediaHost.setShowsOnlyActiveMedia(false); + ViewGroup hostView = mMediaHost.getHostView(); addView(hostView); - ViewGroup.LayoutParams layoutParams = hostView.getLayoutParams(); + int sidePaddings = getResources().getDimensionPixelSize( + R.dimen.quick_settings_side_margins); + int bottomPadding = getResources().getDimensionPixelSize( + R.dimen.quick_settings_expanded_bottom_margin); + MarginLayoutParams layoutParams = (MarginLayoutParams) hostView.getLayoutParams(); layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT; layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT; + layoutParams.bottomMargin = bottomPadding; + hostView.setLayoutParams(layoutParams); + hostView.setPadding(sidePaddings, hostView.getPaddingTop(), sidePaddings, + hostView.getPaddingBottom()); } private final QSMediaBrowser.Callback mMediaBrowserCallback = new QSMediaBrowser.Callback() { @@ -297,7 +312,11 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne int numChildren = getChildCount(); for (int i = 0; i < numChildren; i++) { View child = getChildAt(i); - if (child.getVisibility() != View.GONE) height += child.getMeasuredHeight(); + if (child.getVisibility() != View.GONE) { + height += child.getMeasuredHeight(); + MarginLayoutParams layoutParams = (MarginLayoutParams) child.getLayoutParams(); + height += layoutParams.topMargin + layoutParams.bottomMargin; + } } setMeasuredDimension(getMeasuredWidth(), height); } @@ -529,7 +548,7 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne public void setListening(boolean listening) { if (mListening == listening) return; - mMediaHiearchyManager.setShouldListen(listening); + mMediaHost.setShouldListen(listening); if (mTileLayout != null) { mQSLogger.logAllTilesChangeListening(listening, getDumpableTag(), mCachedSpecs); mTileLayout.setListening(listening); diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java index 8dfa22f115d7d..194cd5903427b 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java @@ -27,21 +27,19 @@ import android.view.View; import android.view.ViewGroup; import android.widget.LinearLayout; -import androidx.annotation.NonNull; import com.android.internal.logging.UiEventLogger; import com.android.systemui.Dependency; import com.android.systemui.R; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dump.DumpManager; import com.android.systemui.plugins.ActivityStarter; -import com.android.systemui.media.MediaData; import com.android.systemui.media.MediaHierarchyManager; +import com.android.systemui.media.MediaHost; import com.android.systemui.plugins.qs.QSTile; import com.android.systemui.plugins.qs.QSTile.SignalState; import com.android.systemui.plugins.qs.QSTile.State; import com.android.systemui.qs.customize.QSCustomizer; import com.android.systemui.qs.logging.QSLogger; -import com.android.systemui.statusbar.NotificationMediaManager; import com.android.systemui.tuner.TunerService; import com.android.systemui.tuner.TunerService.Tunable; import com.android.systemui.util.Utils; @@ -61,7 +59,6 @@ public class QuickQSPanel extends QSPanel { private static final String TAG = "QuickQSPanel"; // Start it at 6 so a non-zero value can be obtained statically. private static int sDefaultMaxTiles = 6; - private final NotificationMediaManager mNotificationMediaManager; private boolean mDisabledByPolicy; private int mMaxTiles; @@ -76,19 +73,7 @@ public class QuickQSPanel extends QSPanel { private QSTileLayout mHorizontalTileLayout; private QSTileLayout mRegularTileLayout; private int mLastOrientation = -1; - private ViewGroup mMediaHost; - private final NotificationMediaManager.MediaListener mMediaCallback - = new NotificationMediaManager.MediaListener() { - @Override - public void onMediaDataLoaded(@NonNull String key, @NonNull MediaData data) { - switchTileLayout(); - } - - @Override - public void onMediaDataRemoved(@NonNull String key) { - switchTileLayout(); - } - }; + private int mMediaBottomMargin; @Inject public QuickQSPanel( @@ -97,14 +82,12 @@ public class QuickQSPanel extends QSPanel { DumpManager dumpManager, BroadcastDispatcher broadcastDispatcher, QSLogger qsLogger, - MediaHierarchyManager mediaHierarchyManager, - NotificationMediaManager notificationMediaManager, + MediaHost mediaHost, ActivityStarter activityStarter, UiEventLogger uiEventLogger ) { - super(context, attrs, dumpManager, broadcastDispatcher, qsLogger, - mediaHierarchyManager, activityStarter, uiEventLogger); - mNotificationMediaManager = notificationMediaManager; + super(context, attrs, dumpManager, broadcastDispatcher, qsLogger, mediaHost, + activityStarter, uiEventLogger); if (mFooter != null) { removeView(mFooter.getView()); } @@ -114,59 +97,105 @@ public class QuickQSPanel extends QSPanel { } removeView((View) mTileLayout); } + mMediaBottomMargin = getResources().getDimensionPixelSize( + R.dimen.quick_settings_media_extra_bottom_margin); mUsingMediaPlayer = Utils.useQsMediaPlayer(context); if (mUsingMediaPlayer) { - mNotificationMediaManager.addCallback(mMediaCallback); mHorizontalLinearLayout = new LinearLayout(mContext); mHorizontalLinearLayout.setOrientation(LinearLayout.HORIZONTAL); mHorizontalLinearLayout.setClipChildren(false); mHorizontalLinearLayout.setClipToPadding(false); - mTileLayout = new DoubleLineTileLayout(context, mUiEventLogger); - mHorizontalTileLayout = mTileLayout; + DoubleLineTileLayout horizontalTileLayout = new DoubleLineTileLayout(context, + mUiEventLogger); + horizontalTileLayout.setPaddingRelative( + horizontalTileLayout.getPaddingStart(), + horizontalTileLayout.getPaddingTop(), + horizontalTileLayout.getPaddingEnd(), + mContext.getResources().getDimensionPixelSize( + R.dimen.qqs_horizonal_tile_padding_bottom)); + mHorizontalTileLayout = horizontalTileLayout; mRegularTileLayout = new HeaderTileLayout(context, mUiEventLogger); - LayoutParams lp = new LayoutParams(0, LayoutParams.MATCH_PARENT, 1); + LayoutParams lp = new LayoutParams(0, LayoutParams.WRAP_CONTENT, 1); int marginSize = (int) mContext.getResources().getDimension(R.dimen.qqs_media_spacing); - lp.setMarginEnd(0); - lp.setMarginStart(marginSize); - mHorizontalLinearLayout.addView((View) mTileLayout, lp); + lp.setMarginStart(0); + lp.setMarginEnd(marginSize); + lp.gravity = Gravity.CENTER_VERTICAL; + mHorizontalLinearLayout.addView((View) mHorizontalTileLayout, lp); sDefaultMaxTiles = getResources().getInteger(R.integer.quick_qs_panel_max_columns); + boolean useHorizontal = shouldUseHorizontalTileLayout(); + mTileLayout = useHorizontal ? mHorizontalTileLayout : mRegularTileLayout; mTileLayout.setListening(mListening); addView(mHorizontalLinearLayout, 0 /* Between brightness and footer */); - ((View) mRegularTileLayout).setVisibility(View.GONE); + ((View) mRegularTileLayout).setVisibility(!useHorizontal ? View.VISIBLE : View.GONE); + mHorizontalLinearLayout.setVisibility(useHorizontal ? View.VISIBLE : View.GONE); addView((View) mRegularTileLayout, 0); super.setPadding(0, 0, 0, 0); - reAttachMediaHost(); + applySideMargins(mHorizontalLinearLayout); + applyBottomMargin((View) mRegularTileLayout); } else { sDefaultMaxTiles = getResources().getInteger(R.integer.quick_qs_panel_max_columns); mTileLayout = new HeaderTileLayout(context, mUiEventLogger); mTileLayout.setListening(mListening); addView((View) mTileLayout, 0 /* Between brightness and footer */); super.setPadding(0, 0, 0, 0); + applyBottomMargin((View) mTileLayout); } } + private void applyBottomMargin(View view) { + int margin = getResources().getDimensionPixelSize(R.dimen.qs_header_tile_margin_bottom); + MarginLayoutParams layoutParams = (MarginLayoutParams) view.getLayoutParams(); + layoutParams.bottomMargin = margin; + view.setLayoutParams(layoutParams); + } + + private void applySideMargins(View view) { + int margin = getResources().getDimensionPixelSize(R.dimen.qs_header_tile_margin_horizontal); + MarginLayoutParams layoutParams = (MarginLayoutParams) view.getLayoutParams(); + layoutParams.setMarginStart(margin); + layoutParams.setMarginEnd(margin); + view.setLayoutParams(layoutParams); + } + private void reAttachMediaHost() { - ViewGroup newParent = shouldUseHorizontalTileLayout() ? mHorizontalLinearLayout : this; - ViewGroup currentParent = (ViewGroup) mMediaHost.getParent(); + if (mMediaHost == null) { + return; + } + boolean horizontal = shouldUseHorizontalTileLayout(); + ViewGroup host = mMediaHost.getHostView(); + ViewGroup newParent = horizontal ? mHorizontalLinearLayout : this; + ViewGroup currentParent = (ViewGroup) host.getParent(); if (currentParent != newParent) { if (currentParent != null) { - currentParent.removeView(mMediaHost); + currentParent.removeView(host); } - newParent.addView(mMediaHost); - ViewGroup.LayoutParams layoutParams = mMediaHost.getLayoutParams(); + newParent.addView(host); + LinearLayout.LayoutParams layoutParams = (LayoutParams) host.getLayoutParams(); layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT; - layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT; + layoutParams.width = horizontal ? 0 : ViewGroup.LayoutParams.MATCH_PARENT; + layoutParams.weight = horizontal ? 1.5f : 0; + layoutParams.bottomMargin = mMediaBottomMargin; + int marginStart = horizontal + ? getResources().getDimensionPixelSize(R.dimen.qs_header_tile_margin_horizontal) + : 0; + layoutParams.setMarginStart(marginStart); } } @Override protected void addMediaHostView() { - mMediaHost = mMediaHiearchyManager.createMediaHost( - MediaHierarchyManager.LOCATION_QQS); + mMediaHost.setVisibleChangedListener((visible) -> { + switchTileLayout(); + return null; + }); + mMediaHost.init(MediaHierarchyManager.LOCATION_QQS); + mMediaHost.setExpanded(false); + mMediaHost.setShowsOnlyActiveMedia(true); + reAttachMediaHost(); } @Override @@ -188,7 +217,6 @@ public class QuickQSPanel extends QSPanel { protected void onDetachedFromWindow() { super.onDetachedFromWindow(); Dependency.get(TunerService.class).removeTunable(mNumTiles); - mNotificationMediaManager.removeCallback(mMediaCallback); } @Override @@ -257,7 +285,7 @@ public class QuickQSPanel extends QSPanel { } private boolean shouldUseHorizontalTileLayout() { - return mNotificationMediaManager.hasActiveMedia() + return mMediaHost.getVisible() && getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE; } @@ -381,7 +409,7 @@ public class QuickQSPanel extends QSPanel { setClipChildren(false); setClipToPadding(false); LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, - LayoutParams.MATCH_PARENT); + LayoutParams.WRAP_CONTENT); lp.gravity = Gravity.CENTER_HORIZONTAL; setLayoutParams(lp); } @@ -394,6 +422,7 @@ public class QuickQSPanel extends QSPanel { @Override public void onFinishInflate(){ + super.onFinishInflate(); updateResources(); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java index b14697d831ac0..7901363ef93c5 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java @@ -336,23 +336,6 @@ public class QuickStatusBarHeader extends RelativeLayout implements com.android.internal.R.dimen.quick_qs_offset_height); mSystemIconsView.setLayoutParams(mSystemIconsView.getLayoutParams()); - FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) getLayoutParams(); - - if (mQsDisabled) { - lp.height = resources.getDimensionPixelSize( - com.android.internal.R.dimen.quick_qs_offset_height); - } else if (useQsMediaPlayer(mContext) && mHeaderQsPanel.usesHorizontalLayout()) { - lp.height = Math.max(getMinimumHeight(), - resources.getDimensionPixelSize( - com.android.internal.R.dimen.quick_qs_total_height_with_media)); - } else { - lp.height = Math.max(getMinimumHeight(), - resources.getDimensionPixelSize( - com.android.internal.R.dimen.quick_qs_total_height)); - } - - setLayoutParams(lp); - updateStatusIconAlphaAnimator(); updateHeaderTextContainerAlphaAnimator(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java index 63ba57ba12536..9f4932e74eaae 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java @@ -106,7 +106,6 @@ public class NotificationMediaManager implements Dumpable { } private final NotificationEntryManager mEntryManager; - private final MediaDataManager mMediaDataManager; @Nullable private Lazy mNotificationShadeWindowController; @@ -206,35 +205,17 @@ public class NotificationMediaManager implements Dumpable { mNotificationShadeWindowController = notificationShadeWindowController; mEntryManager = notificationEntryManager; mMainExecutor = mainExecutor; - mMediaDataManager = mediaDataManager; - mediaDataManager.setListener(new MediaListener() { - @Override - public void onMediaDataLoaded(@NonNull String key, @NonNull MediaData metadata) { - ArrayList callbacks = new ArrayList<>(mMediaListeners); - for (int i = 0; i < callbacks.size(); i++) { - callbacks.get(i).onMediaDataLoaded(key, metadata); - } - } - - @Override - public void onMediaDataRemoved(@NonNull String key) { - ArrayList callbacks = new ArrayList<>(mMediaListeners); - for (int i = 0; i < callbacks.size(); i++) { - callbacks.get(i).onMediaDataRemoved(key); - } - } - }); notificationEntryManager.addNotificationEntryListener(new NotificationEntryListener() { @Override public void onPendingEntryAdded(NotificationEntry entry) { - mMediaDataManager.onNotificationAdded(entry.getKey(), entry.getSbn()); + mediaDataManager.onNotificationAdded(entry.getKey(), entry.getSbn()); } @Override public void onPreEntryUpdated(NotificationEntry entry) { - mMediaDataManager.onNotificationAdded(entry.getKey(), entry.getSbn()); + mediaDataManager.onNotificationAdded(entry.getKey(), entry.getSbn()); } @Override @@ -254,7 +235,7 @@ public class NotificationMediaManager implements Dumpable { boolean removedByUser, int reason) { onNotificationRemoved(entry.getKey()); - mMediaDataManager.onNotificationRemoved(entry.getKey()); + mediaDataManager.onNotificationRemoved(entry.getKey()); } }); @@ -719,13 +700,6 @@ public class NotificationMediaManager implements Dumpable { mProcessArtworkTasks.remove(task); } - /** - * Are there any media notifications active? - */ - public boolean hasActiveMedia() { - return mMediaDataManager.hasActiveMedia(); - } - /** * {@link AsyncTask} to prepare album art for use as backdrop on lock screen. */ @@ -781,15 +755,5 @@ public class NotificationMediaManager implements Dumpable { */ default void onPrimaryMetadataOrStateChanged(MediaMetadata metadata, @PlaybackState.State int state) {} - - /** - * Called whenever there's new MediaData Loaded for the consumption in views - */ - default void onMediaDataLoaded(@NonNull String key, @NonNull MediaData data) {} - - /** - * Called whenever a previously existing Media notification was removed - */ - default void onMediaDataRemoved(@NonNull String key) {} } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.java index 1d01db453da49..9f8e04468116f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.java @@ -125,7 +125,6 @@ public class NotificationSectionsManager implements StackScrollAlgorithm.Section StatusBarStateController statusBarStateController, ConfigurationController configurationController, PeopleHubViewAdapter peopleHubViewAdapter, - MediaHierarchyManager mediaHiearchyManager, KeyguardMediaController keyguardMediaController, NotificationSectionsFeatureManager sectionsFeatureManager) { mActivityStarter = activityStarter; diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.java index 128d6e5612f14..c2a58855a9ce9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.java @@ -43,6 +43,7 @@ import com.android.systemui.Dependency; import com.android.systemui.SysuiTestCase; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dump.DumpManager; +import com.android.systemui.media.MediaHost; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.qs.QSTileView; import com.android.systemui.qs.customize.QSCustomizer; @@ -50,7 +51,6 @@ import com.android.systemui.qs.logging.QSLogger; import com.android.systemui.qs.tileimpl.QSTileImpl; import com.android.systemui.statusbar.notification.NotificationEntryManager; import com.android.systemui.statusbar.policy.SecurityController; -import com.android.systemui.util.concurrency.DelayableExecutor; import org.junit.Before; import org.junit.Test; @@ -62,7 +62,6 @@ import java.io.FileDescriptor; import java.io.PrintWriter; import java.io.StringWriter; import java.util.Collections; -import java.util.concurrent.Executor; @RunWith(AndroidTestingRunner.class) @RunWithLooper @@ -90,9 +89,7 @@ public class QSPanelTest extends SysuiTestCase { @Mock private QSTileView mQSTileView; @Mock - private Executor mForegroundExecutor; - @Mock - private DelayableExecutor mBackgroundExecutor; + private MediaHost mMediaHost; @Mock private LocalBluetoothManager mLocalBluetoothManager; @Mock @@ -116,8 +113,7 @@ public class QSPanelTest extends SysuiTestCase { mTestableLooper.runWithLooper(() -> { mMetricsLogger = mDependency.injectMockDependency(MetricsLogger.class); mQsPanel = new QSPanel(mContext, null, mDumpManager, mBroadcastDispatcher, - mQSLogger, mForegroundExecutor, mBackgroundExecutor, - mLocalBluetoothManager, mActivityStarter, mEntryManager, mUiEventLogger); + mQSLogger, mMediaHost, mActivityStarter, mUiEventLogger); // Provides a parent with non-zero size for QSPanel mParentView = new FrameLayout(mContext); mParentView.addView(mQsPanel); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java index cdf2b6faf98c6..8f2011e229440 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java @@ -44,6 +44,7 @@ import androidx.test.filters.SmallTest; import com.android.systemui.ActivityStarterDelegate; import com.android.systemui.SysuiTestCase; +import com.android.systemui.media.KeyguardMediaController; import com.android.systemui.media.MediaHierarchyManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.StatusBarState; @@ -75,7 +76,7 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { @Mock private StatusBarStateController mStatusBarStateController; @Mock private ConfigurationController mConfigurationController; @Mock private PeopleHubViewAdapter mPeopleHubAdapter; - @Mock private MediaHierarchyManager mMediaHierarchyManager; + @Mock private KeyguardMediaController mKeyguardMediaController; @Mock private NotificationSectionsFeatureManager mSectionsFeatureManager; @Mock private NotificationRowComponent mNotificationRowComponent; @Mock private ActivatableNotificationViewController mActivatableNotificationViewController; @@ -88,15 +89,13 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { when(mNotificationRowComponent.getActivatableNotificationViewController()).thenReturn( mActivatableNotificationViewController ); - when(mMediaHierarchyManager.createMediaHost(any())).thenReturn( - new UniqueObjectHost(getContext())); mSectionsManager = new NotificationSectionsManager( mActivityStarterDelegate, mStatusBarStateController, mConfigurationController, mPeopleHubAdapter, - mMediaHierarchyManager, + mKeyguardMediaController, mSectionsFeatureManager ); // Required in order for the header inflation to work properly From f0f7495c92cc01a794cdcdd3e7e55f2710f1209b Mon Sep 17 00:00:00 2001 From: Selim Cinek Date: Tue, 21 Apr 2020 11:45:16 -0700 Subject: [PATCH 05/14] 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 --- .../SystemUI/res/layout/media_carousel.xml | 4 + .../SystemUI/res/layout/qs_media_panel.xml | 15 +- packages/SystemUI/res/xml/media_scene.xml | 199 ++++++++++-- .../systemui/media/KeyguardMediaController.kt | 2 +- .../systemui/media/MediaControlPanel.java | 219 +++---------- .../systemui/media/MediaHierarchyManager.kt | 301 +++++++++++++++--- .../com/android/systemui/media/MediaHost.kt | 77 ++++- .../android/systemui/qs/QSContainerImpl.java | 2 +- .../com/android/systemui/qs/QSFragment.java | 32 +- .../src/com/android/systemui/qs/QSPanel.java | 15 +- .../com/android/systemui/qs/QuickQSPanel.java | 7 +- .../systemui/qs/QuickStatusBarHeader.java | 4 + .../notification/row/ExpandableView.java | 4 + .../notification/stack/MediaHeaderView.java | 5 + .../stack/NotificationStackScrollLayout.java | 6 +- .../NotificationPanelViewController.java | 4 + .../com/android/systemui/qs/QSPanelTest.java | 2 +- 17 files changed, 621 insertions(+), 277 deletions(-) diff --git a/packages/SystemUI/res/layout/media_carousel.xml b/packages/SystemUI/res/layout/media_carousel.xml index 627cf88bc9ad4..fe8f2bb76f558 100644 --- a/packages/SystemUI/res/layout/media_carousel.xml +++ b/packages/SystemUI/res/layout/media_carousel.xml @@ -21,6 +21,8 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:scrollbars="none" + android:clipChildren="false" + android:clipToPadding="false" android:fillViewport="true" > diff --git a/packages/SystemUI/res/layout/qs_media_panel.xml b/packages/SystemUI/res/layout/qs_media_panel.xml index 2b30096e2c042..36ae2251d9631 100644 --- a/packages/SystemUI/res/layout/qs_media_panel.xml +++ b/packages/SystemUI/res/layout/qs_media_panel.xml @@ -21,11 +21,22 @@ android:id="@+id/qs_media_controls" android:layout_width="match_parent" android:layout_height="wrap_content" - android:background="@drawable/qs_media_background" + android:clipChildren="false" + android:clipToPadding="false" android:gravity="center_horizontal|fill_vertical" - android:paddingBottom="@dimen/qs_media_panel_outer_padding" app:layoutDescription="@xml/media_scene"> + + + app:duration="1000" > + + + + + + + + + + + + + + + + + + + + + + + + @@ -74,9 +168,9 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginEnd="@dimen/qs_media_panel_outer_padding" - android:layout_marginTop="3dp" + android:layout_marginTop="17dp" android:layout_marginStart="16dp" - app:layout_constraintTop_toTopOf="@id/album_art" + app:layout_constraintTop_toBottomOf="@+id/app_name" app:layout_constraintStart_toEndOf="@id/album_art" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0"/> @@ -98,7 +192,8 @@ android:id="@+id/media_progress_bar" android:layout_width="0dp" android:layout_height="wrap_content" - app:layout_constraintTop_toBottomOf="@id/album_art" + android:layout_marginTop="3dp" + app:layout_constraintTop_toBottomOf="@id/header_artist" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" /> @@ -107,10 +202,10 @@ android:id="@+id/notification_media_progress_time" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginTop="35dp" + android:layout_marginTop="38dp" android:layout_marginEnd="@dimen/qs_media_panel_outer_padding" android:layout_marginStart="@dimen/qs_media_panel_outer_padding" - app:layout_constraintTop_toBottomOf="@id/album_art" + app:layout_constraintTop_toBottomOf="@id/header_artist" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" /> @@ -120,48 +215,66 @@ android:layout_width="48dp" android:layout_height="48dp" android:layout_marginTop="5dp" - android:layout_marginStart="@dimen/qs_media_panel_outer_padding" - app:layout_constraintVertical_chainStyle="packed" + android:layout_marginStart="4dp" + android:layout_marginEnd="4dp" + android:layout_marginBottom="@dimen/qs_media_panel_outer_padding" + app:layout_constraintHorizontal_chainStyle="packed" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toLeftOf="@id/action1" - app:layout_constraintTop_toBottomOf="@id/notification_media_progress_time" > + app:layout_constraintTop_toBottomOf="@id/notification_media_progress_time" + app:layout_constraintBottom_toBottomOf="parent"> + app:layout_constraintTop_toTopOf="@id/action0" + app:layout_constraintBottom_toBottomOf="parent"> + app:layout_constraintTop_toTopOf="@id/action0" + app:layout_constraintBottom_toBottomOf="parent"> + app:layout_constraintTop_toTopOf="@id/action0" + android:layout_marginBottom="@dimen/qs_media_panel_outer_padding" + app:layout_constraintBottom_toBottomOf="parent"> + app:layout_constraintTop_toTopOf="@id/action0" + app:layout_constraintBottom_toBottomOf="parent"> @@ -178,7 +291,7 @@ @@ -215,9 +330,10 @@ android:id="@+id/header_title" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginTop="3dp" + android:layout_marginTop="17dp" android:layout_marginStart="16dp" - app:layout_constraintTop_toTopOf="@id/album_art" + app:layout_constraintTop_toBottomOf="@id/app_name" + app:layout_constraintBottom_toTopOf="@id/header_artist" app:layout_constraintStart_toEndOf="@id/album_art" app:layout_constraintEnd_toStartOf="@id/action0" app:layout_constraintHorizontal_bias="0"/> @@ -228,9 +344,11 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginTop="3dp" + android:layout_marginBottom="24dp" app:layout_constraintTop_toBottomOf="@id/header_title" app:layout_constraintStart_toStartOf="@id/header_title" app:layout_constraintEnd_toStartOf="@id/action0" + app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintHorizontal_bias="0"/> @@ -261,52 +379,67 @@ android:id="@+id/action0" android:layout_width="48dp" android:layout_height="48dp" - android:layout_marginTop="5dp" android:layout_marginStart="4dp" + android:layout_marginTop="16dp" android:visibility="gone" - app:layout_constraintVertical_chainStyle="packed" + app:layout_constraintHorizontal_chainStyle="packed" + app:layout_constraintTop_toBottomOf="@id/app_name" app:layout_constraintLeft_toRightOf="@id/header_title" app:layout_constraintRight_toLeftOf="@id/action1" - app:layout_constraintTop_toTopOf="@id/album_art"> + > + > + > + > + > diff --git a/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt b/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt index ae7860e8f2928..b87a1db67ebe7 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 @@ class KeyguardMediaController @Inject constructor( 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 0297c90a29b9c..673fafa0b1a3a 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 @@ package com.android.systemui.media; 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.SeekBar; 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 com.android.systemui.util.concurrency.DelayableExecutor; 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 @@ public class MediaControlPanel { 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 @@ public class MediaControlPanel { private MediaDevice mDevice; protected ComponentName mServiceComponent; private boolean mIsRegistered = false; + private final List mKeyFrames; private String mKey; public static final String MEDIA_PREFERENCES = "media_control_prefs"; @@ -184,8 +178,6 @@ public class MediaControlPanel { * @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 @@ public class MediaControlPanel { 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 @@ public class MediaControlPanel { * 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 @@ public class MediaControlPanel { } 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 @@ public class MediaControlPanel { 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 @@ public class MediaControlPanel { }); } + 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 @@ public class MediaControlPanel { 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 @@ public class MediaControlPanel { int i = 0; List 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 @@ public class MediaControlPanel { } }); 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 @@ public class MediaControlPanel { // 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 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); } } @@ -501,120 +490,6 @@ public class MediaControlPanel { return (state.getState() == PlaybackState.STATE_PLAYING); } - /** - * 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 c9630f4ff79c1..2e41ffb249608 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 @@ class MediaHierarchyManager @Inject constructor( @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 = mutableMapOf() private val mediaHosts = arrayOfNulls(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 @@ class MediaHierarchyManager @Inject constructor( } } + 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 @@ class MediaHierarchyManager @Inject constructor( } } }) + statusBarStateController.addCallback(object : StatusBarStateController.StateListener { + override fun onStateChanged(newState: Int) { + updateDesiredLocation() + } + }) } private fun inflateMediaCarousel(): ViewGroup { @@ -112,7 +170,7 @@ class MediaHierarchyManager @Inject constructor( 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 @@ class MediaHierarchyManager @Inject constructor( * @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 @@ class MediaHierarchyManager @Inject constructor( && (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 @@ class MediaHierarchyManager @Inject constructor( /** * 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 @@ class MediaHierarchyManager @Inject constructor( * 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 7df61f2091f17..42bf0e8608554 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 @@ class MediaHost @Inject constructor( 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 088630c771d1c..6b0775f6c2d71 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 @@ public class QSContainerImpl extends FrameLayout { 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 5b09267a9e682..865fd079234e5 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 androidx.annotation.VisibleForTesting; 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.phone.NotificationsQuickSettingsContainer; 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 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca */ private int mState; private QSContainerImplController mQSContainerImplController; + private int[] mTmpLocation = new int[2]; @Inject public QSFragment(RemoteInputQuickSettingsDisabler remoteInputQsDisabler, @@ -377,8 +380,7 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca 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 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca 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 0222969efdaf8..3e87b0215b85b 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 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne 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 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne BroadcastDispatcher broadcastDispatcher, QSLogger qsLogger, MediaHost mediaHost, - ActivityStarter activityStarter, UiEventLogger uiEventLogger ) { super(context, attrs); @@ -157,7 +155,6 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne mQSLogger = qsLogger; mDumpManager = dumpManager; mBroadcastDispatcher = broadcastDispatcher; - mActivityStarter = activityStarter; mUiEventLogger = uiEventLogger; setOrientation(VERTICAL); @@ -186,7 +183,6 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne findViewById(R.id.brightness_slider), mBroadcastDispatcher); } - @Override protected void onFinishInflate() { super.onFinishInflate(); @@ -198,7 +194,7 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne 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 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne @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 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne int iconColor = Color.DKGRAY; int bgColor = Color.LTGRAY; player.setMediaSession(token, desc, iconColor, bgColor, browser.getAppIntent(), - pkgName); + pkgName);*/ } }; @@ -856,6 +853,10 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne } } + 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 194cd5903427b..dfd385dda8e50 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 @@ public class QuickQSPanel extends QSPanel { 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 @@ public class QuickQSPanel extends QSPanel { 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 7901363ef93c5..3b2bea8f80f26 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 @@ public class QuickStatusBarHeader extends RelativeLayout implements mNextAlarmTextView.setSelected(true); } + public QuickQSPanel getHeaderQsPanel() { + return mHeaderQsPanel; + } + private List getIgnoredIconSlots() { ArrayList 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 5797944298d4b..0831c0b667976 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 @@ public abstract class ExpandableView extends FrameLayout implements Dumpable { 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 82560f55509ae..65594efd064c9 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 @@ public class MediaHeaderView extends ActivatableNotificationView { 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 7f32c004808a8..db9faf5c7ace4 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 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd */ @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 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd @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 aa6baffbeba40..8161a83f403c8 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 @@ public class NotificationPanelViewController extends PanelViewController { // 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); diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.java index c2a58855a9ce9..6c543c73456cc 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.java @@ -113,7 +113,7 @@ public class QSPanelTest extends SysuiTestCase { mTestableLooper.runWithLooper(() -> { mMetricsLogger = mDependency.injectMockDependency(MetricsLogger.class); mQsPanel = new QSPanel(mContext, null, mDumpManager, mBroadcastDispatcher, - mQSLogger, mMediaHost, mActivityStarter, mUiEventLogger); + mQSLogger, mMediaHost, mUiEventLogger); // Provides a parent with non-zero size for QSPanel mParentView = new FrameLayout(mContext); mParentView.addView(mQsPanel); From c54367124cd59746c260197bb9dfe830484c97b2 Mon Sep 17 00:00:00 2001 From: Selim Cinek Date: Mon, 27 Apr 2020 15:15:44 -0700 Subject: [PATCH 06/14] Work back in media uri loading that was changed in the refactor Rebuilt ag/11180565 and fixed a few cases while doing so. This also includes the fixes from ag/11270945 Test: play music from spotify Bug: 154137987 Change-Id: Ib912df716d754f451b68b289920a2e138977b9bd --- .../systemui/media/MediaControlPanel.java | 60 ++++++-- .../com/android/systemui/media/MediaData.kt | 7 +- .../systemui/media/MediaDataManager.kt | 137 +++++++++++++----- .../notification/row/HybridGroupManager.java | 4 +- 4 files changed, 158 insertions(+), 50 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java index 673fafa0b1a3a..6621ca13583c0 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java +++ b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java @@ -26,10 +26,13 @@ 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.Canvas; +import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; +import android.graphics.drawable.Icon; import android.graphics.drawable.RippleDrawable; -import android.media.MediaMetadata; import android.media.session.MediaController; import android.media.session.MediaController.PlaybackInfo; import android.media.session.MediaSession; @@ -52,7 +55,10 @@ 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.Utils; import com.android.settingslib.media.LocalMediaManager; import com.android.settingslib.media.MediaDevice; import com.android.settingslib.media.MediaOutputSliceConstants; @@ -104,6 +110,8 @@ public class MediaControlPanel { private boolean mIsRegistered = false; private final List mKeyFrames; private String mKey; + private int mAlbumArtSize; + private int mAlbumArtRadius; public static final String MEDIA_PREFERENCES = "media_control_prefs"; public static final String MEDIA_PREFERENCE_KEY = "browser_components"; @@ -112,13 +120,6 @@ public class MediaControlPanel { private boolean mIsRemotePlayback; private QSMediaBrowser mQSMediaBrowser; - // URI fields to try loading album art from - private static final String[] ART_URIS = { - MediaMetadata.METADATA_KEY_ALBUM_ART_URI, - MediaMetadata.METADATA_KEY_ART_URI, - MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI - }; - private final MediaController.Callback mSessionCallback = new MediaController.Callback() { @Override public void onSessionDestroyed() { @@ -210,6 +211,13 @@ public class MediaControlPanel { SeekBar bar = getView().findViewById(R.id.media_progress_bar); bar.setOnSeekBarChangeListener(mSeekBarViewModel.getSeekBarListener()); bar.setOnTouchListener(mSeekBarViewModel.getSeekBarTouchListener()); + loadDimens(); + } + + private void loadDimens() { + mAlbumArtRadius = mContext.getResources().getDimensionPixelSize( + Utils.getThemeAttr(mContext, android.R.attr.dialogCornerRadius)); + mAlbumArtSize = mContext.getResources().getDimensionPixelSize(R.dimen.qs_media_album_size); } /** @@ -296,7 +304,10 @@ public class MediaControlPanel { } ImageView albumView = mMediaNotifView.findViewById(R.id.album_art); - albumView.setImageDrawable(data.getArtwork()); + // TODO: migrate this to a view with rounded corners instead of baking the rounding + // into the bitmap + Drawable artwork = createRoundedBitmap(data.getArtwork()); + albumView.setImageDrawable(artwork); // App icon ImageView appIcon = mMediaNotifView.requireViewById(R.id.icon); @@ -400,6 +411,37 @@ public class MediaControlPanel { makeActive(); } + private Drawable createRoundedBitmap(Icon icon) { + if (icon == null) { + return null; + } + // Let's scale down the View, such that the content always nicely fills the view. + // ThumbnailUtils actually scales it down such that it may not be filled for odd aspect + // ratios + Drawable drawable = icon.loadDrawable(mContext); + float aspectRatio = drawable.getIntrinsicHeight() / (float) drawable.getIntrinsicWidth(); + Rect bounds; + if (aspectRatio > 1.0f) { + bounds = new Rect(0, 0, mAlbumArtSize, (int) (mAlbumArtSize * aspectRatio)); + } else { + bounds = new Rect(0, 0, (int) (mAlbumArtSize / aspectRatio), mAlbumArtSize); + } + if (bounds.width() > mAlbumArtSize || bounds.height() > mAlbumArtSize) { + float offsetX = (bounds.width() - mAlbumArtSize) / 2.0f; + float offsetY = (bounds.height() - mAlbumArtSize) / 2.0f; + bounds.offset((int) -offsetX,(int) -offsetY); + } + drawable.setBounds(bounds); + Bitmap scaled = Bitmap.createBitmap(mAlbumArtSize, mAlbumArtSize, + Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(scaled); + drawable.draw(canvas); + RoundedBitmapDrawable artwork = RoundedBitmapDrawableFactory.create( + mContext.getResources(), scaled); + artwork.setCornerRadius(mAlbumArtRadius); + return artwork; + } + /** * Updates the keyframe visibility such that only views that are not visible actually go * through a transition and fade in. diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaData.kt b/packages/SystemUI/src/com/android/systemui/media/MediaData.kt index 8165362ef677c..6a2646170e854 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaData.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaData.kt @@ -18,6 +18,7 @@ package com.android.systemui.media import android.app.PendingIntent import android.graphics.drawable.Drawable +import android.graphics.drawable.Icon import android.media.session.MediaSession /** State of a media view. */ @@ -27,9 +28,9 @@ data class MediaData( val backgroundColor: Int, val app: String?, val appIcon: Drawable?, - val artist: String?, - val song: String?, - val artwork: Drawable?, + val artist: CharSequence?, + val song: CharSequence?, + val artwork: Icon?, val actions: List, val actionsToShowInCompact: List, val packageName: String?, diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt index 227e8e7b3986f..e7d0f7ec1a375 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt @@ -17,29 +17,45 @@ package com.android.systemui.media import android.app.Notification +import android.content.ContentResolver import android.content.Context import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.ImageDecoder +import android.graphics.Rect import android.graphics.drawable.Drawable +import android.graphics.drawable.Icon import android.media.MediaMetadata import android.media.session.MediaSession -import android.media.session.PlaybackState +import android.net.Uri import android.provider.Settings import android.service.notification.StatusBarNotification -import androidx.core.graphics.drawable.RoundedBitmapDrawable -import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory +import android.text.TextUtils +import android.util.Log import com.android.internal.util.ContrastColorUtil -import com.android.settingslib.Utils -import com.android.systemui.R import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main -import com.android.systemui.statusbar.NotificationMediaManager import com.android.systemui.statusbar.notification.MediaNotificationProcessor +import com.android.systemui.statusbar.notification.row.HybridGroupManager +import java.io.IOException import java.util.* import java.util.concurrent.Executor import javax.inject.Inject import javax.inject.Singleton import kotlin.collections.LinkedHashMap +// URI fields to try loading album art from +private val ART_URIS = arrayOf( + MediaMetadata.METADATA_KEY_ALBUM_ART_URI, + MediaMetadata.METADATA_KEY_ART_URI, + MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI +) + +private const val TAG = "MediaDataManager" + +private val LOADING = MediaData(false, 0, 0, null, null, null, null, null, + emptyList(), emptyList(), null, null, null) + /** * A class that facilitates management and loading of Media Data, ready for binding. */ @@ -52,20 +68,8 @@ class MediaDataManager @Inject constructor( ) { private val listeners: MutableSet = mutableSetOf() - private var albumArtSize: Int = 0 - private var albumArtRadius: Int = 0 private val mediaEntries: LinkedHashMap = LinkedHashMap() - init { - loadDimens() - } - - private fun loadDimens() { - albumArtRadius = context.resources.getDimensionPixelSize( - Utils.getThemeAttr(context, android.R.attr.dialogCornerRadius)) - albumArtSize = context.resources.getDimensionPixelSize(R.dimen.qs_media_album_size) - } - fun onNotificationAdded(key: String, sbn: StatusBarNotification) { if (isMediaNotification(sbn)) { if (!mediaEntries.containsKey(key)) { @@ -111,9 +115,31 @@ class MediaDataManager @Inject constructor( if (artworkBitmap == null) { artworkBitmap = metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART) } - // TODO: load media data from uri - if (artworkBitmap != null) { + if (artworkBitmap == null) { + artworkBitmap = loadBitmapFromUri(metadata) + } + val artWorkIcon = if (artworkBitmap == null) { + notif.getLargeIcon() + } else { + Icon.createWithBitmap(artworkBitmap) + } + if (artWorkIcon != null) { // If we have art, get colors from that + if (artworkBitmap == null) { + if (artWorkIcon.type == Icon.TYPE_BITMAP + || artWorkIcon.type == Icon.TYPE_ADAPTIVE_BITMAP) { + artworkBitmap = artWorkIcon.bitmap + } else { + val drawable: Drawable = artWorkIcon.loadDrawable(context) + artworkBitmap = Bitmap.createBitmap( + drawable.intrinsicWidth, + drawable.intrinsicHeight, + Bitmap.Config.ARGB_8888) + val canvas = Canvas(artworkBitmap) + drawable.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight) + drawable.draw(canvas) + } + } val p = MediaNotificationProcessor.generateArtworkPaletteBuilder(artworkBitmap) .generate() val swatch = MediaNotificationProcessor.findBackgroundSwatch(p) @@ -126,16 +152,6 @@ class MediaDataManager @Inject constructor( isDark) fgColor = ContrastColorUtil.ensureTextContrast(fgColor, bgColor, isDark) - // Album art - var artwork: RoundedBitmapDrawable? = null - if (artworkBitmap != null) { - val original = artworkBitmap.copy(Bitmap.Config.ARGB_8888, true) - val scaled = Bitmap.createScaledBitmap(original, albumArtSize, albumArtSize, - false) - artwork = RoundedBitmapDrawableFactory.create(context.resources, scaled) - artwork.cornerRadius = albumArtRadius.toFloat() - } - // App name val builder = Notification.Builder.recoverBuilder(context, notif) val app = builder.loadHeaderAppName() @@ -144,10 +160,19 @@ class MediaDataManager @Inject constructor( val smallIconDrawable: Drawable = sbn.notification.smallIcon.loadDrawable(context) // Song name - val song: String = metadata.getString(MediaMetadata.METADATA_KEY_TITLE) + var song: CharSequence? = metadata.getString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE) + if (song == null) { + song = metadata.getString(MediaMetadata.METADATA_KEY_TITLE) + } + if (song == null) { + song = HybridGroupManager.resolveTitle(notif) + } // Artist name - val artist: String = metadata.getString(MediaMetadata.METADATA_KEY_ARTIST) + var artist: CharSequence? = metadata.getString(MediaMetadata.METADATA_KEY_ARTIST) + if (artist == null) { + artist = HybridGroupManager.resolveText(notif) + } // Control buttons val actionIcons: MutableList = ArrayList() @@ -167,12 +192,55 @@ class MediaDataManager @Inject constructor( foregroundExcecutor.execute { onMediaDataLoaded(key, MediaData(true, fgColor, bgColor, app, smallIconDrawable, artist, - song, artwork, actionIcons, actionsToShowCollapsed, sbn.packageName, token, + song, artWorkIcon, actionIcons, actionsToShowCollapsed, sbn.packageName, token, notif.contentIntent)) } } + /** + * Load a bitmap from the various Art metadata URIs + */ + private fun loadBitmapFromUri(metadata: MediaMetadata): Bitmap? { + for (uri in ART_URIS) { + val uriString = metadata.getString(uri) + if (!TextUtils.isEmpty(uriString)) { + val albumArt = loadBitmapFromUri(Uri.parse(uriString)) + if (albumArt != null) { + Log.d(TAG, "loaded art from $uri") + break + } + } + } + return null + } + + /** + * Load a bitmap from a URI + * @param uri the uri to load + * @return bitmap, or null if couldn't be loaded + */ + private fun loadBitmapFromUri(uri: Uri): Bitmap? { + // 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; + } + + val source = ImageDecoder.createSource(context.getContentResolver(), uri) + return try { + ImageDecoder.decodeBitmap(source) + } catch (e: IOException) { + e.printStackTrace() + null + } + } + fun onMediaDataLoaded(key: String, data: MediaData) { if (mediaEntries.containsKey(key)) { // Otherwise this was removed already @@ -236,6 +304,3 @@ class MediaDataManager @Inject constructor( fun onMediaDataRemoved(key: String) {} } } - -private val LOADING = MediaData(false, 0, 0, null, null, null, null, null, - emptyList(), emptyList(), null, null, null) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridGroupManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridGroupManager.java index 0ccebc130b1d9..56f8e087d64dc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridGroupManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridGroupManager.java @@ -109,7 +109,7 @@ public class HybridGroupManager { } @Nullable - private CharSequence resolveText(Notification notification) { + public static CharSequence resolveText(Notification notification) { CharSequence contentText = notification.extras.getCharSequence(Notification.EXTRA_TEXT); if (contentText == null) { contentText = notification.extras.getCharSequence(Notification.EXTRA_BIG_TEXT); @@ -118,7 +118,7 @@ public class HybridGroupManager { } @Nullable - private CharSequence resolveTitle(Notification notification) { + public static CharSequence resolveTitle(Notification notification) { CharSequence titleText = notification.extras.getCharSequence(Notification.EXTRA_TITLE); if (titleText == null) { titleText = notification.extras.getCharSequence(Notification.EXTRA_TITLE_BIG); From 098baf4dd92a94d17445981862079b1ed796b622 Mon Sep 17 00:00:00 2001 From: Selim Cinek Date: Mon, 27 Apr 2020 19:02:06 -0700 Subject: [PATCH 07/14] Migrated away from view detaching as a signal Previously the seekbar update stopped pretty much immediately since the Lifecycle was set to destroyed whenever detatched. We're now listening whenever a view exists. Similarly, were we making the view inactive whenever reattaching, which happens constantly now Test: add any player, observe progress bar updating Bug: 154137987 Change-Id: I5bac0822ceb282999db35c797d7355127b1daf31 --- .../systemui/media/MediaControlPanel.java | 32 ++++--------------- .../systemui/media/MediaHierarchyManager.kt | 1 + 2 files changed, 8 insertions(+), 25 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java index 6621ca13583c0..5a5cd6dc3a50b 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java +++ b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java @@ -16,8 +16,6 @@ package com.android.systemui.media; -import static com.android.systemui.util.SysuiLifecycle.viewAttachLifecycle; - import android.app.PendingIntent; import android.content.ComponentName; import android.content.Context; @@ -41,7 +39,6 @@ import android.service.media.MediaBrowserService; import android.util.Log; import android.view.LayoutInflater; import android.view.View; -import android.view.View.OnAttachStateChangeListener; import android.view.ViewGroup; import android.widget.ImageButton; import android.widget.ImageView; @@ -139,17 +136,6 @@ public class MediaControlPanel { } }; - private final OnAttachStateChangeListener mStateListener = new OnAttachStateChangeListener() { - @Override - public void onViewAttachedToWindow(View unused) { - makeActive(); - } - @Override - public void onViewDetachedFromWindow(View unused) { - makeInactive(); - } - }; - private final LocalMediaManager.DeviceCallback mDeviceCallback = new LocalMediaManager.DeviceCallback() { @Override @@ -191,29 +177,25 @@ public class MediaControlPanel { 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 - // attach/detach of views instead of inflating them in the constructor, which would allow - // mStateListener to be unregistered in detach. - mMediaNotifView.addOnAttachStateChangeListener(mStateListener); mLocalMediaManager = routeManager; mForegroundExecutor = foregroundExecutor; mBackgroundExecutor = backgroundExecutor; mActivityStarter = activityStarter; mSeekBarViewModel = new SeekBarViewModel(backgroundExecutor); mSeekBarObserver = new SeekBarObserver(getView()); - // Can't use the viewAttachLifecycle of media player because remove/add is used to adjust - // priority of players. As soon as it is removed, the lifecycle will end and the seek bar - // will stop updating. So, use the lifecycle of the parent instead. - // TODO: this parent is also detached, need to fix that - mSeekBarViewModel.getProgress().observe(viewAttachLifecycle(parent), mSeekBarObserver); + // TODO: we should pause this whenever the screen is off / panel is collapsed etc. + mSeekBarViewModel.getProgress().observeForever(mSeekBarObserver); SeekBar bar = getView().findViewById(R.id.media_progress_bar); bar.setOnSeekBarChangeListener(mSeekBarViewModel.getSeekBarListener()); bar.setOnTouchListener(mSeekBarViewModel.getSeekBarTouchListener()); loadDimens(); } + public void onDestroy() { + mSeekBarViewModel.getProgress().removeObserver(mSeekBarObserver); + makeInactive(); + } + private void loadDimens() { mAlbumArtRadius = mContext.getResources().getDimensionPixelSize( Utils.getThemeAttr(mContext, android.R.attr.dialogCornerRadius)); diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt index 2e41ffb249608..e584cccb24e22 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt @@ -133,6 +133,7 @@ class MediaHierarchyManager @Inject constructor( val removed = mediaPlayers.remove(key) removed?.apply { mediaContent.removeView(removed.view) + removed.onDestroy() } } }) From 9e517f73a5bc70c6cf2c55e7dd0343ab875a52f7 Mon Sep 17 00:00:00 2001 From: Selim Cinek Date: Tue, 28 Apr 2020 12:24:34 -0700 Subject: [PATCH 08/14] Factored out the media view manager from the hiearchyManager It was always supposed to be separate, but we now finally split it in preparation for the Viewpager refactor. Bug: 154137987 Test: add media notification, observe :) Change-Id: I1ae53ab1cb9bb67493fa4175e06687cb2b3b4b78 --- .../systemui/media/MediaHierarchyManager.kt | 124 ++------------- .../com/android/systemui/media/MediaHost.kt | 5 - .../systemui/media/MediaViewManager.kt | 143 ++++++++++++++++++ .../src/com/android/systemui/qs/QSPanel.java | 1 - 4 files changed, 152 insertions(+), 121 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt index e584cccb24e22..673a543d8ac07 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt @@ -21,30 +21,20 @@ 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 import javax.inject.Inject import javax.inject.Singleton @@ -55,18 +45,15 @@ import javax.inject.Singleton @Singleton class MediaHierarchyManager @Inject constructor( private val context: Context, - @Main private val foregroundExecutor: Executor, - @Background private val backgroundExecutor: DelayableExecutor, - private val localBluetoothManager: LocalBluetoothManager?, - private val visualStabilityManager: VisualStabilityManager, private val statusBarStateController: SysuiStatusBarStateController, private val keyguardStateController: KeyguardStateController, private val bypassController: KeyguardBypassController, - private val activityStarter: ActivityStarter, - mediaManager: MediaDataManager + private val mediaViewManager: MediaViewManager ) { private var rootOverlay: ViewGroupOverlay? = null private lateinit var currentState: MediaState + private val mediaCarousel + get() = mediaViewManager.mediaCarousel private var animationStartState: MediaState? = null private var animator = ValueAnimator.ofFloat(0.0f, 1.0f).apply { interpolator = Interpolators.FAST_OUT_SLOW_IN @@ -92,23 +79,11 @@ class MediaHierarchyManager @Inject constructor( }) } private var targetState: MediaState? = null - private val mediaCarousel: ViewGroup - private val mediaContent: ViewGroup - private val mediaPlayers: MutableMap = mutableMapOf() private val mediaHosts = arrayOfNulls(LOCATION_LOCKSCREEN + 1) - private val visualStabilityCallback = ::reorderAllPlayers private var previousLocation = -1 private var desiredLocation = -1 private var currentAttachmentLocation = -1 - var shouldListen = true - set(value) { - field = value - for (player in mediaPlayers.values) { - player.setListening(shouldListen) - } - } - var qsExpansion: Float = 0.0f set(value) { if (field != value) { @@ -122,21 +97,6 @@ class MediaHierarchyManager @Inject constructor( } init { - mediaCarousel = inflateMediaCarousel() - mediaContent = mediaCarousel.requireViewById(R.id.media_carousel) - mediaManager.addListener(object : MediaDataManager.Listener { - override fun onMediaDataLoaded(key: String, data: MediaData) { - updateView(key, data) - } - - override fun onMediaDataRemoved(key: String) { - val removed = mediaPlayers.remove(key) - removed?.apply { - mediaContent.removeView(removed.view) - removed.onDestroy() - } - } - }) statusBarStateController.addCallback(object : StatusBarStateController.StateListener { override fun onStateChanged(newState: Int) { updateDesiredLocation() @@ -144,72 +104,6 @@ class MediaHierarchyManager @Inject constructor( }) } - private fun inflateMediaCarousel(): ViewGroup { - return LayoutInflater.from(context).inflate( - R.layout.media_carousel, UniqueObjectHost(context), false) as ViewGroup - } - - private fun reorderAllPlayers() { - for (mediaPlayer in mediaPlayers.values) { - val view = mediaPlayer.view - if (mediaPlayer.isPlaying && mediaContent.indexOfChild(view) != 0) { - mediaContent.removeView(view) - mediaContent.addView(view, 0) - } - } - updateMediaPaddings() - } - - private fun updateView(key: String, data: MediaData) { - var existingPlayer = mediaPlayers[key] - if (existingPlayer == null) { - // Set up listener for device changes - // TODO: integrate with MediaTransferManager? - val imm = InfoMediaManager(context, data.packageName, - null /* notification */, localBluetoothManager) - val routeManager = LocalMediaManager(context, localBluetoothManager, - imm, data.packageName) - - existingPlayer = MediaControlPanel(context, mediaContent, routeManager, - foregroundExecutor, backgroundExecutor, activityStarter) - mediaPlayers[key] = existingPlayer - val lp = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT) - existingPlayer.view.setLayoutParams(lp) - existingPlayer.setListening(shouldListen) - if (existingPlayer.isPlaying) { - mediaContent.addView(existingPlayer.view, 0) - } else { - mediaContent.addView(existingPlayer.view) - } - } else if (existingPlayer.isPlaying && - mediaContent.indexOfChild(existingPlayer.view) != 0) { - if (visualStabilityManager.isReorderingAllowed) { - mediaContent.removeView(existingPlayer.view) - mediaContent.addView(existingPlayer.view, 0) - } else { - visualStabilityManager.addReorderingAllowedCallback(visualStabilityCallback) - } - } - existingPlayer.bind(data) - updateMediaPaddings() - } - - private fun updateMediaPaddings() { - val padding = context.resources.getDimensionPixelSize(R.dimen.qs_media_padding) - val childCount = mediaContent.childCount - for (i in 0 until childCount) { - val mediaView = mediaContent.getChildAt(i) - val desiredPaddingEnd = if (i == childCount - 1) 0 else padding - val layoutParams = mediaView.layoutParams as ViewGroup.MarginLayoutParams - if (layoutParams.marginEnd != desiredPaddingEnd) { - layoutParams.marginEnd = desiredPaddingEnd - mediaView.layoutParams = layoutParams - } - } - - } - /** * Register a media host and create a view can be attached to a view hierarchy * and where the players will be placed in when the host is the currently desired state. @@ -386,16 +280,16 @@ class MediaHierarchyManager @Inject constructor( 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) + mediaCarousel.setLeftTopRightBottom( + boundsOnScreen.left, + boundsOnScreen.top, + boundsOnScreen.right, + boundsOnScreen.bottom) } currentState = state.copy() + mediaViewManager.applyState(currentState) } private fun updateHostAttachment() { diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt index 42bf0e8608554..12e2743c1333a 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt @@ -75,11 +75,6 @@ class MediaHost @Inject constructor( updateViewVisibility() } - fun setShouldListen(listen: Boolean) { - // TODO: look into listening more - mediaHierarchyManager.shouldListen = listen - } - private fun updateViewVisibility() { if (showsOnlyActiveMedia) { visible = mediaDataManager.hasActiveMedia() diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt new file mode 100644 index 0000000000000..eeecf2bb6ef4a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt @@ -0,0 +1,143 @@ +package com.android.systemui.media + +import android.content.Context +import android.view.LayoutInflater +import android.view.ViewGroup +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.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.statusbar.notification.VisualStabilityManager +import com.android.systemui.util.animation.UniqueObjectHost +import com.android.systemui.util.concurrency.DelayableExecutor +import java.util.concurrent.Executor +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Class that is responsible for keeping the view carousel up to date. + * This also handles changes in state and applies them to the media carousel like the expansion. + */ +@Singleton +class MediaViewManager @Inject constructor( + private val context: Context, + @Main private val foregroundExecutor: Executor, + @Background private val backgroundExecutor: DelayableExecutor, + private val localBluetoothManager: LocalBluetoothManager?, + private val visualStabilityManager: VisualStabilityManager, + private val activityStarter: ActivityStarter, + mediaManager: MediaDataManager +) { + private var targetState: MediaState? = null + val mediaCarousel: ViewGroup + private val mediaContent: ViewGroup + private val mediaPlayers: MutableMap = mutableMapOf() + private val visualStabilityCallback = ::reorderAllPlayers + + private var viewsExpanded = true + set(value) { + if (field != value) { + field = value + for (player in mediaPlayers.values) { + player.setListening(field) + } + } + } + + init { + mediaCarousel = inflateMediaCarousel() + mediaContent = mediaCarousel.requireViewById(R.id.media_carousel) + mediaManager.addListener(object : MediaDataManager.Listener { + override fun onMediaDataLoaded(key: String, data: MediaData) { + updateView(key, data) + } + + override fun onMediaDataRemoved(key: String) { + val removed = mediaPlayers.remove(key) + removed?.apply { + mediaContent.removeView(removed.view) + removed.onDestroy() + } + } + }) + } + + private fun inflateMediaCarousel(): ViewGroup { + return LayoutInflater.from(context).inflate( + R.layout.media_carousel, UniqueObjectHost(context), false) as ViewGroup + } + + private fun reorderAllPlayers() { + for (mediaPlayer in mediaPlayers.values) { + val view = mediaPlayer.view + if (mediaPlayer.isPlaying && mediaContent.indexOfChild(view) != 0) { + mediaContent.removeView(view) + mediaContent.addView(view, 0) + } + } + updateMediaPaddings() + } + + private fun updateView(key: String, data: MediaData) { + var existingPlayer = mediaPlayers[key] + if (existingPlayer == null) { + // Set up listener for device changes + // TODO: integrate with MediaTransferManager? + val imm = InfoMediaManager(context, data.packageName, + null /* notification */, localBluetoothManager) + val routeManager = LocalMediaManager(context, localBluetoothManager, + imm, data.packageName) + + existingPlayer = MediaControlPanel(context, mediaContent, routeManager, + foregroundExecutor, backgroundExecutor, activityStarter) + mediaPlayers[key] = existingPlayer + val lp = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT) + existingPlayer.view.setLayoutParams(lp) + existingPlayer.setListening(viewsExpanded) + if (existingPlayer.isPlaying) { + mediaContent.addView(existingPlayer.view, 0) + } else { + mediaContent.addView(existingPlayer.view) + } + } else if (existingPlayer.isPlaying && + mediaContent.indexOfChild(existingPlayer.view) != 0) { + if (visualStabilityManager.isReorderingAllowed) { + mediaContent.removeView(existingPlayer.view) + mediaContent.addView(existingPlayer.view, 0) + } else { + visualStabilityManager.addReorderingAllowedCallback(visualStabilityCallback) + } + } + existingPlayer.bind(data) + updateMediaPaddings() + } + + private fun updateMediaPaddings() { + val padding = context.resources.getDimensionPixelSize(R.dimen.qs_media_padding) + val childCount = mediaContent.childCount + for (i in 0 until childCount) { + val mediaView = mediaContent.getChildAt(i) + val desiredPaddingEnd = if (i == childCount - 1) 0 else padding + val layoutParams = mediaView.layoutParams as ViewGroup.MarginLayoutParams + if (layoutParams.marginEnd != desiredPaddingEnd) { + layoutParams.marginEnd = desiredPaddingEnd + mediaView.layoutParams = layoutParams + } + } + + } + + fun applyState(state: MediaState) { + for (mediaPlayer in mediaPlayers.values) { + val view = mediaPlayer.view + view.progress = state.expansion + } + viewsExpanded = state.expansion > 0; + } +} \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java index 3e87b0215b85b..80e5071c6b43c 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java @@ -545,7 +545,6 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne public void setListening(boolean listening) { if (mListening == listening) return; - mMediaHost.setShouldListen(listening); if (mTileLayout != null) { mQSLogger.logAllTilesChangeListening(listening, getDumpableTag(), mCachedSpecs); mTileLayout.setListening(listening); From 3df592e5a8bc0fe9ce94eb5fae1fc9959f87df4c Mon Sep 17 00:00:00 2001 From: Selim Cinek Date: Tue, 28 Apr 2020 13:51:43 -0700 Subject: [PATCH 09/14] When animating between states, animate the view width Test: add media notification observe no flickery animation Bug: 154137987 Change-Id: I909dd4427813b34426cc7d7fc08f298761f134bc --- .../SystemUI/res/layout/media_carousel.xml | 1 - .../SystemUI/res/layout/qs_media_panel.xml | 11 +- packages/SystemUI/res/values/ids.xml | 20 +++ packages/SystemUI/res/xml/media_scene.xml | 20 +-- .../systemui/media/LayoutAnimationHelper.kt | 115 ++++++++++++++++++ .../systemui/media/MediaControlPanel.java | 32 +++++ .../systemui/media/MediaHierarchyManager.kt | 7 +- .../systemui/media/MediaViewManager.kt | 22 +++- .../notification/AnimatableProperty.java | 96 +++++++++++++++ .../notification/PropertyAnimator.java | 22 +++- 10 files changed, 325 insertions(+), 21 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/media/LayoutAnimationHelper.kt diff --git a/packages/SystemUI/res/layout/media_carousel.xml b/packages/SystemUI/res/layout/media_carousel.xml index fe8f2bb76f558..f888ca11b5fba 100644 --- a/packages/SystemUI/res/layout/media_carousel.xml +++ b/packages/SystemUI/res/layout/media_carousel.xml @@ -23,7 +23,6 @@ android:scrollbars="none" android:clipChildren="false" android:clipToPadding="false" - android:fillViewport="true" > + + diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml index 04640f418e975..fef8300395b38 100644 --- a/packages/SystemUI/res/values/ids.xml +++ b/packages/SystemUI/res/values/ids.xml @@ -70,6 +70,26 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/packages/SystemUI/res/xml/media_scene.xml b/packages/SystemUI/res/xml/media_scene.xml index ab9b7d9a284d7..6f99d9a675f12 100644 --- a/packages/SystemUI/res/xml/media_scene.xml +++ b/packages/SystemUI/res/xml/media_scene.xml @@ -145,7 +145,7 @@ android:id="@+id/media_seamless" android:layout_width="0dp" android:layout_height="wrap_content" - app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintEnd_toEndOf="@id/view_width" app:layout_constraintTop_toTopOf="parent" app:layout_constraintWidth_min="60dp" android:layout_marginTop="@dimen/qs_media_panel_outer_padding" @@ -172,7 +172,7 @@ android:layout_marginStart="16dp" app:layout_constraintTop_toBottomOf="@+id/app_name" app:layout_constraintStart_toEndOf="@id/album_art" - app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintEnd_toEndOf="@id/view_width" app:layout_constraintHorizontal_bias="0"/> @@ -184,7 +184,7 @@ android:layout_marginTop="3dp" app:layout_constraintTop_toBottomOf="@id/header_title" app:layout_constraintStart_toStartOf="@id/header_title" - app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintEnd_toEndOf="@id/view_width" app:layout_constraintHorizontal_bias="0"/> @@ -195,7 +195,7 @@ android:layout_marginTop="3dp" app:layout_constraintTop_toBottomOf="@id/header_artist" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintEnd_toEndOf="@id/view_width" /> @@ -306,7 +306,7 @@ android:id="@+id/media_seamless" android:layout_width="0dp" android:layout_height="wrap_content" - app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintEnd_toEndOf="@id/view_width" app:layout_constraintTop_toTopOf="parent" app:layout_constraintWidth_min="60dp" android:layout_marginTop="@dimen/qs_media_panel_outer_padding" @@ -358,7 +358,7 @@ android:layout_height="wrap_content" app:layout_constraintTop_toBottomOf="@id/album_art" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintEnd_toEndOf="@id/view_width" android:visibility="gone" /> @@ -371,7 +371,7 @@ android:layout_marginStart="@dimen/qs_media_panel_outer_padding" app:layout_constraintTop_toBottomOf="@id/album_art" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintEnd_toEndOf="@id/view_width" android:visibility="gone" /> @@ -438,7 +438,7 @@ android:layout_marginTop="18dp" app:layout_constraintTop_toBottomOf="@id/app_name" app:layout_constraintLeft_toRightOf="@id/action3" - app:layout_constraintRight_toRightOf="parent" + app:layout_constraintRight_toRightOf="@id/view_width" > diff --git a/packages/SystemUI/src/com/android/systemui/media/LayoutAnimationHelper.kt b/packages/SystemUI/src/com/android/systemui/media/LayoutAnimationHelper.kt new file mode 100644 index 0000000000000..a366725a43986 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/LayoutAnimationHelper.kt @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2020 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 + */ +package com.android.systemui.media + +import android.graphics.Rect +import android.view.View +import android.view.ViewGroup +import android.view.ViewTreeObserver +import com.android.systemui.statusbar.notification.AnimatableProperty +import com.android.systemui.statusbar.notification.PropertyAnimator +import com.android.systemui.statusbar.notification.stack.AnimationProperties + +/** + * A utility class that helps with animations of bound changes designed for motionlayout which + * doesn't work together with regular changeBounds. + */ +class LayoutAnimationHelper { + + private val layout: ViewGroup + private var sizeAnimationPending = false + private val desiredBounds = mutableMapOf() + private val animationProperties = AnimationProperties() + private val layoutListener = object : View.OnLayoutChangeListener { + override fun onLayoutChange(v: View?, left: Int, top: Int, right: Int, bottom: Int, + oldLeft: Int, oldTop: Int, oldRight: Int, oldBottom: Int) { + v?.let { + if (v.alpha == 0.0f || v.visibility == View.GONE || oldLeft - oldRight == 0 || + oldTop - oldBottom == 0) { + return + } + if (oldLeft != left || oldTop != top || oldBottom != bottom || oldRight != right) { + val rect = desiredBounds.getOrPut(v, { Rect() }) + rect.set(left, top, right, bottom) + onDesiredLocationChanged(v, rect) + } + } + } + } + + constructor(layout: ViewGroup) { + this.layout = layout + val childCount = this.layout.childCount + for (i in 0 until childCount) { + val child = this.layout.getChildAt(i) + child.addOnLayoutChangeListener(layoutListener) + } + } + + private fun onDesiredLocationChanged(v: View, rect: Rect) { + if (!sizeAnimationPending) { + applyBounds(v, rect, animate = false) + } + // We need to reapply the current bounds in every frame since the layout may override + // the layout bounds making this view jump and not all calls to apply bounds actually + // reapply them, for example if there's already an animator to the same target + reapplyProperty(v, AnimatableProperty.ABSOLUTE_X); + reapplyProperty(v, AnimatableProperty.ABSOLUTE_Y); + reapplyProperty(v, AnimatableProperty.WIDTH); + reapplyProperty(v, AnimatableProperty.HEIGHT); + } + + private fun reapplyProperty(v: View, property: AnimatableProperty) { + property.property.set(v, property.property.get(v)) + } + + private fun applyBounds(v: View, newBounds: Rect, animate: Boolean) { + PropertyAnimator.setProperty(v, AnimatableProperty.ABSOLUTE_X, newBounds.left.toFloat(), + animationProperties, animate) + PropertyAnimator.setProperty(v, AnimatableProperty.ABSOLUTE_Y, newBounds.top.toFloat(), + animationProperties, animate) + PropertyAnimator.setProperty(v, AnimatableProperty.WIDTH, newBounds.width().toFloat(), + animationProperties, animate) + PropertyAnimator.setProperty(v, AnimatableProperty.HEIGHT, newBounds.height().toFloat(), + animationProperties, animate) + } + + private fun startBoundAnimation(v: View) { + val target = desiredBounds[v] ?: return + applyBounds(v, target, animate = true) + } + + fun animatePendingSizeChange(duration: Long, delay: Long) { + animationProperties.duration = duration + animationProperties.delay = delay + if (!sizeAnimationPending) { + sizeAnimationPending = true + layout.viewTreeObserver.addOnPreDrawListener ( + object : ViewTreeObserver.OnPreDrawListener { + override fun onPreDraw(): Boolean { + layout.viewTreeObserver.removeOnPreDrawListener(this) + sizeAnimationPending = false + val childCount = layout.childCount + for (i in 0 until childCount) { + val child = layout.getChildAt(i) + startBoundAnimation(child) + } + return true + } + }) + } + } +} \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java index 5a5cd6dc3a50b..0b0bfc62ed747 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java +++ b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java @@ -36,6 +36,7 @@ import android.media.session.MediaController.PlaybackInfo; import android.media.session.MediaSession; import android.media.session.PlaybackState; import android.service.media.MediaBrowserService; +import android.util.DisplayMetrics; import android.util.Log; import android.view.LayoutInflater; import android.view.View; @@ -93,6 +94,7 @@ public class MediaControlPanel { private final Executor mForegroundExecutor; protected final Executor mBackgroundExecutor; private final ActivityStarter mActivityStarter; + private final LayoutAnimationHelper mLayoutAnimationHelper; private Context mContext; private MotionLayout mMediaNotifView; @@ -109,6 +111,7 @@ public class MediaControlPanel { private String mKey; private int mAlbumArtSize; private int mAlbumArtRadius; + private int mViewWidth; public static final String MEDIA_PREFERENCES = "media_control_prefs"; public static final String MEDIA_PREFERENCE_KEY = "browser_components"; @@ -176,6 +179,7 @@ public class MediaControlPanel { LayoutInflater inflater = LayoutInflater.from(mContext); mMediaNotifView = (MotionLayout) inflater.inflate(R.layout.qs_media_panel, parent, false); mBackground = mMediaNotifView.findViewById(R.id.media_background); + mLayoutAnimationHelper = new LayoutAnimationHelper(mMediaNotifView); mKeyFrames = mMediaNotifView.getDefinedTransitions().get(0).getKeyFrameList(); mLocalMediaManager = routeManager; mForegroundExecutor = foregroundExecutor; @@ -732,4 +736,32 @@ public class MediaControlPanel { * Called when a player can't be resumed to give it an opportunity to hide or remove itself */ protected void removePlayer() { } + + public void setDimension(int newWidth, int newHeight, boolean animate, long duration, + long startDelay) { + // Let's remeasure if our width changed. Our height is dependent on the expansion, so we + // won't animate if it changed + if (newWidth != mViewWidth) { + if (animate) { + mLayoutAnimationHelper.animatePendingSizeChange(duration, startDelay); + } + setViewWidth(newWidth); + mMediaNotifView.layout(0, 0, newWidth, mMediaNotifView.getMeasuredHeight()); + } + } + + protected void setViewWidth(int newWidth) { + ConstraintSet expandedSet = mMediaNotifView.getConstraintSet(R.id.expanded); + ConstraintSet collapsedSet = mMediaNotifView.getConstraintSet(R.id.collapsed); + collapsedSet.setGuidelineBegin(R.id.view_width, newWidth); + expandedSet.setGuidelineBegin(R.id.view_width, newWidth); + DisplayMetrics displayMetrics = getContext().getResources().getDisplayMetrics(); + int widthSpec = View.MeasureSpec.makeMeasureSpec(displayMetrics.widthPixels, + View.MeasureSpec.AT_MOST); + int heightSpec = View.MeasureSpec.makeMeasureSpec(displayMetrics.heightPixels, + View.MeasureSpec.AT_MOST); + mMediaNotifView.setMinimumWidth(displayMetrics.widthPixels); + mMediaNotifView.measure(widthSpec, heightSpec); + mViewWidth = newWidth; + } } diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt index 673a543d8ac07..fc141bf256f0e 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt @@ -24,9 +24,6 @@ import android.content.Context import android.view.View import android.view.ViewGroup import android.view.ViewGroupOverlay -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.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.StatusBarState @@ -167,6 +164,7 @@ class MediaHierarchyManager @Inject constructor( return; } updateTargetState() + var animate = false if (isCurrentlyInGuidedTransformation()) { applyTargetStateIfNotAnimating() } else if (shouldAnimateTransition(currentHost, previousHost)) { @@ -175,9 +173,12 @@ class MediaHierarchyManager @Inject constructor( animationStartState = currentState.copy() adjustAnimatorForTransition(previousLocation, desiredLocation) animator.start() + animate = true } else { cancelAnimationAndApplyDesiredState() } + mediaViewManager.performTransition(targetState, animate, animator.duration, + animator.startDelay) } private fun shouldAnimateTransition(currentHost: MediaHost, previousHost: MediaHost): Boolean { diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt index eeecf2bb6ef4a..636e4e46a1b0c 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt @@ -33,7 +33,6 @@ class MediaViewManager @Inject constructor( private val activityStarter: ActivityStarter, mediaManager: MediaDataManager ) { - private var targetState: MediaState? = null val mediaCarousel: ViewGroup private val mediaContent: ViewGroup private val mediaPlayers: MutableMap = mutableMapOf() @@ -140,4 +139,25 @@ class MediaViewManager @Inject constructor( } viewsExpanded = state.expansion > 0; } + + /** + * @param targetState the target state we're transitioning to + * @param animate should this be animated + */ + fun performTransition(targetState: MediaState?, animate: Boolean, duration: Long, + startDelay: Long) { + if (targetState == null) { + return + } + val newWidth = targetState.boundsOnScreen.width() + val newHeight = targetState.boundsOnScreen.height() + remeasureViews(newWidth, newHeight, animate, duration, startDelay) + } + + private fun remeasureViews(newWidth: Int, newHeight: Int, animate: Boolean, duration: Long, + startDelay: Long) { + for (mediaPlayer in mediaPlayers.values) { + mediaPlayer.setDimension(newWidth, newHeight, animate, duration, startDelay) + } + } } \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/AnimatableProperty.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/AnimatableProperty.java index 75b41ca3e162e..eee9cc683e2b2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/AnimatableProperty.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/AnimatableProperty.java @@ -16,7 +16,9 @@ package com.android.systemui.statusbar.notification; +import android.graphics.drawable.Drawable; import android.util.FloatProperty; +import android.util.Log; import android.util.Property; import android.view.View; @@ -35,6 +37,100 @@ public abstract class AnimatableProperty { public static final AnimatableProperty Y = AnimatableProperty.from(View.Y, R.id.y_animator_tag, R.id.y_animator_tag_start_value, R.id.y_animator_tag_end_value); + /** + * Similar to X, however this doesn't allow for any other modifications other than from this + * property. When using X, it's possible that the view is laid out during the animation, + * which could break the continuity + */ + public static final AnimatableProperty ABSOLUTE_X = AnimatableProperty.from( + new FloatProperty("ViewAbsoluteX") { + @Override + public void setValue(View view, float value) { + view.setTag(R.id.absolute_x_current_value, value); + View.X.set(view, value); + } + + @Override + public Float get(View view) { + Object tag = view.getTag(R.id.absolute_x_current_value); + if (tag instanceof Float) { + return (Float) tag; + } + return View.X.get(view); + } + }, + R.id.absolute_x_animator_tag, + R.id.absolute_x_animator_start_tag, + R.id.absolute_x_animator_end_tag); + + /** + * Similar to Y, however this doesn't allow for any other modifications other than from this + * property. When using X, it's possible that the view is laid out during the animation, + * which could break the continuity + */ + public static final AnimatableProperty ABSOLUTE_Y = AnimatableProperty.from( + new FloatProperty("ViewAbsoluteY") { + @Override + public void setValue(View view, float value) { + view.setTag(R.id.absolute_y_current_value, value); + View.Y.set(view, value); + } + + @Override + public Float get(View view) { + Object tag = view.getTag(R.id.absolute_y_current_value); + if (tag instanceof Float) { + return (Float) tag; + } + return View.Y.get(view); + } + }, + R.id.absolute_y_animator_tag, + R.id.absolute_y_animator_start_tag, + R.id.absolute_y_animator_end_tag); + + public static final AnimatableProperty WIDTH = AnimatableProperty.from( + new FloatProperty("ViewWidth") { + @Override + public void setValue(View view, float value) { + view.setTag(R.id.view_width_current_value, value); + view.setRight((int) (view.getLeft() + value)); + } + + @Override + public Float get(View view) { + Object tag = view.getTag(R.id.view_width_current_value); + if (tag instanceof Float) { + return (Float) tag; + } + return (float) view.getWidth(); + } + }, + R.id.view_width_animator_tag, + R.id.view_width_animator_start_tag, + R.id.view_width_animator_end_tag); + + public static final AnimatableProperty HEIGHT = AnimatableProperty.from( + new FloatProperty("ViewHeight") { + @Override + public void setValue(View view, float value) { + view.setTag(R.id.view_height_current_value, value); + view.setBottom((int) (view.getTop() + value)); + } + + @Override + public Float get(View view) { + Object tag = view.getTag(R.id.view_height_current_value); + if (tag instanceof Float) { + return (Float) tag; + } + return (float) view.getHeight(); + } + }, + R.id.view_height_animator_tag, + R.id.view_height_animator_start_tag, + R.id.view_height_animator_end_tag); + public abstract int getAnimationStartTag(); public abstract int getAnimationEndTag(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/PropertyAnimator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/PropertyAnimator.java index 1f9d3af70b4fc..b1b6a1c12a0a2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/PropertyAnimator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/PropertyAnimator.java @@ -34,13 +34,20 @@ import com.android.systemui.statusbar.notification.stack.ViewState; */ public class PropertyAnimator { + /** + * Set a property on a view, updating its value, even if it's already animating. + * The @param animated can be used to request an animation. + * If the view isn't animated, this utility will update the current animation if existent, + * such that the end value will point to @param newEndValue or apply it directly if there's + * no animation. + */ public static void setProperty(final T view, AnimatableProperty animatableProperty, float newEndValue, AnimationProperties properties, boolean animated) { int animatorTag = animatableProperty.getAnimatorTag(); ValueAnimator previousAnimator = ViewState.getChildTag(view, animatorTag); if (previousAnimator != null || animated) { - startAnimation(view, animatableProperty, newEndValue, properties); + startAnimation(view, animatableProperty, newEndValue, animated ? properties : null); } else { // no new animation needed, let's just apply the value animatableProperty.getProperty().set(view, newEndValue); @@ -60,8 +67,8 @@ public class PropertyAnimator { } int animatorTag = animatableProperty.getAnimatorTag(); ValueAnimator previousAnimator = ViewState.getChildTag(view, animatorTag); - AnimationFilter filter = properties.getAnimationFilter(); - if (!filter.shouldAnimateProperty(property)) { + AnimationFilter filter = properties != null ? properties.getAnimationFilter() : null; + if (filter == null || !filter.shouldAnimateProperty(property)) { // just a local update was performed if (previousAnimator != null) { // we need to increase all animation keyframes of the previous animator by the @@ -82,6 +89,14 @@ public class PropertyAnimator { } Float currentValue = property.get(view); + AnimatorListenerAdapter listener = properties.getAnimationFinishListener(property); + if (currentValue.equals(newEndValue)) { + // Skip the animation! + if (listener != null) { + listener.onAnimationEnd(null); + } + return; + } ValueAnimator animator = ValueAnimator.ofFloat(currentValue, newEndValue); animator.addUpdateListener( animation -> property.set(view, (Float) animation.getAnimatedValue())); @@ -96,7 +111,6 @@ public class PropertyAnimator { || previousAnimator.getAnimatedFraction() == 0)) { animator.setStartDelay(properties.delay); } - AnimatorListenerAdapter listener = properties.getAnimationFinishListener(property); if (listener != null) { animator.addListener(listener); } From 54809628f98982cbfbf1a3be678876d5aacf386e Mon Sep 17 00:00:00 2001 From: Selim Cinek Date: Thu, 30 Apr 2020 19:04:44 -0700 Subject: [PATCH 10/14] Measuring the Media Views now properly the first time its created Instead of only knowing the measurement when its attached, we need to know before otherwise the layout will be broken Bug: 154137987 Test: atest SystemUITests Change-Id: I8a4d90a7d933f477df65a5f26942059b0f0920da --- .../systemui/media/MediaControlPanel.java | 31 +++--- .../systemui/media/MediaHierarchyManager.kt | 60 +++++++----- .../com/android/systemui/media/MediaHost.kt | 34 ++++++- .../systemui/media/MediaMeasurementManager.kt | 59 ++++++++++++ .../systemui/media/MediaViewManager.kt | 54 +++++++---- .../util/animation/MeasurementCache.kt | 87 +++++++++++++++++ .../util/animation/UniqueObjectHost.kt | 41 -------- .../util/animation/UniqueObjectHostView.kt | 95 +++++++++++++++++++ .../NotificationSectionsManagerTest.java | 2 - 9 files changed, 361 insertions(+), 102 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/media/MediaMeasurementManager.kt create mode 100644 packages/SystemUI/src/com/android/systemui/util/animation/MeasurementCache.kt delete mode 100644 packages/SystemUI/src/com/android/systemui/util/animation/UniqueObjectHost.kt create mode 100644 packages/SystemUI/src/com/android/systemui/util/animation/UniqueObjectHostView.kt diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java index 0b0bfc62ed747..3d638dd9e211e 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java +++ b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java @@ -112,6 +112,7 @@ public class MediaControlPanel { private int mAlbumArtSize; private int mAlbumArtRadius; private int mViewWidth; + private MediaMeasurementInput mLastMeasureInput; public static final String MEDIA_PREFERENCES = "media_control_prefs"; public static final String MEDIA_PREFERENCE_KEY = "browser_components"; @@ -737,31 +738,31 @@ public class MediaControlPanel { */ protected void removePlayer() { } - public void setDimension(int newWidth, int newHeight, boolean animate, long duration, + public void remeasure(@Nullable MediaMeasurementInput input, boolean animate, long duration, long startDelay) { // Let's remeasure if our width changed. Our height is dependent on the expansion, so we // won't animate if it changed - if (newWidth != mViewWidth) { + if (input != null && !input.sameAs(mLastMeasureInput)) { + mLastMeasureInput = input; if (animate) { mLayoutAnimationHelper.animatePendingSizeChange(duration, startDelay); } - setViewWidth(newWidth); - mMediaNotifView.layout(0, 0, newWidth, mMediaNotifView.getMeasuredHeight()); + remeasureInternal(input); + mMediaNotifView.layout(0, 0, mMediaNotifView.getMeasuredWidth(), + mMediaNotifView.getMeasuredHeight()); } } - protected void setViewWidth(int newWidth) { + public MediaMeasurementInput getLastMeasureInput() { + return mLastMeasureInput; + } + + private void remeasureInternal(MediaMeasurementInput input) { ConstraintSet expandedSet = mMediaNotifView.getConstraintSet(R.id.expanded); ConstraintSet collapsedSet = mMediaNotifView.getConstraintSet(R.id.collapsed); - collapsedSet.setGuidelineBegin(R.id.view_width, newWidth); - expandedSet.setGuidelineBegin(R.id.view_width, newWidth); - DisplayMetrics displayMetrics = getContext().getResources().getDisplayMetrics(); - int widthSpec = View.MeasureSpec.makeMeasureSpec(displayMetrics.widthPixels, - View.MeasureSpec.AT_MOST); - int heightSpec = View.MeasureSpec.makeMeasureSpec(displayMetrics.heightPixels, - View.MeasureSpec.AT_MOST); - mMediaNotifView.setMinimumWidth(displayMetrics.widthPixels); - mMediaNotifView.measure(widthSpec, heightSpec); - mViewWidth = newWidth; + int width = input.getWidth(); + collapsedSet.setGuidelineBegin(R.id.view_width, width); + expandedSet.setGuidelineBegin(R.id.view_width, width); + mMediaNotifView.measure(input.getWidthMeasureSpec(), input.getHeightMeasureSpec()); } } diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt index fc141bf256f0e..26a61046c6df9 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt @@ -31,7 +31,7 @@ import com.android.systemui.statusbar.SysuiStatusBarStateController 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.animation.UniqueObjectHostView import javax.inject.Inject import javax.inject.Singleton @@ -45,7 +45,8 @@ class MediaHierarchyManager @Inject constructor( private val statusBarStateController: SysuiStatusBarStateController, private val keyguardStateController: KeyguardStateController, private val bypassController: KeyguardBypassController, - private val mediaViewManager: MediaViewManager + private val mediaViewManager: MediaViewManager, + private val mediaMeasurementProvider: MediaMeasurementManager ) { private var rootOverlay: ViewGroupOverlay? = null private lateinit var currentState: MediaState @@ -108,7 +109,7 @@ class MediaHierarchyManager @Inject constructor( * @return the hostView associated with this location */ fun register(mediaObject: MediaHost) : ViewGroup { - val viewHost = createUniqueObjectHost() + val viewHost = createUniqueObjectHost(mediaObject) mediaObject.hostView = viewHost; mediaHosts[mediaObject.location] = mediaObject if (mediaObject.location == desiredLocation) { @@ -123,8 +124,10 @@ class MediaHierarchyManager @Inject constructor( return viewHost } - private fun createUniqueObjectHost(): UniqueObjectHost { - val viewHost = UniqueObjectHost(context) + private fun createUniqueObjectHost(host: MediaState): UniqueObjectHostView { + val viewHost = UniqueObjectHostView(context) + viewHost.measurementCache = mediaMeasurementProvider.obtainCache(host) + viewHost.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener { override fun onViewAttachedToWindow(p0: View?) { if (rootOverlay == null) { @@ -148,12 +151,16 @@ class MediaHierarchyManager @Inject constructor( val isNewView = this.desiredLocation == -1 this.desiredLocation = desiredLocation // Let's perform a transition - performTransition(applyImmediately = isNewView) + val animate = shouldAnimateTransition(desiredLocation, previousLocation) + val (animDuration, delay) = getAnimationParams(previousLocation, desiredLocation) + mediaViewManager.onDesiredStateChanged(getHost(desiredLocation)?.currentState, + animate, animDuration, delay) + performTransitionToNewLocation(isNewView, animate) } } - private fun performTransition(applyImmediately: Boolean) { - if (previousLocation < 0 || applyImmediately) { + private fun performTransitionToNewLocation(isNewView: Boolean, animate: Boolean) { + if (previousLocation < 0 || isNewView) { cancelAnimationAndApplyDesiredState() return } @@ -161,29 +168,27 @@ class MediaHierarchyManager @Inject constructor( val previousHost = getHost(previousLocation) if (currentHost == null || previousHost == null) { cancelAnimationAndApplyDesiredState() - return; } updateTargetState() - var animate = false if (isCurrentlyInGuidedTransformation()) { applyTargetStateIfNotAnimating() - } else if (shouldAnimateTransition(currentHost, previousHost)) { + } else if (animate) { animator.cancel() // Let's animate to the new position, starting from the current position animationStartState = currentState.copy() - adjustAnimatorForTransition(previousLocation, desiredLocation) + adjustAnimatorForTransition(desiredLocation, previousLocation) animator.start() - animate = true } else { cancelAnimationAndApplyDesiredState() } - mediaViewManager.performTransition(targetState, animate, animator.duration, - animator.startDelay) } - private fun shouldAnimateTransition(currentHost: MediaHost, previousHost: MediaHost): Boolean { - if (currentHost.location == LOCATION_QQS - && previousHost.location == LOCATION_LOCKSCREEN + private fun shouldAnimateTransition( + @MediaLocation currentLocation: Int, + @MediaLocation previousLocation: Int + ): Boolean { + if (currentLocation == LOCATION_QQS + && previousLocation == LOCATION_LOCKSCREEN && (statusBarStateController.leaveOpenOnKeyguardHide() || statusBarStateController.state == StatusBarState.SHADE_LOCKED)) { // Usually listening to the isShown is enough to determine this, but there is some @@ -193,7 +198,16 @@ class MediaHierarchyManager @Inject constructor( return mediaCarousel.isShown || animator.isRunning } - private fun adjustAnimatorForTransition(previousLocation: Int, desiredLocation: Int) { + private fun adjustAnimatorForTransition(desiredLocation: Int, previousLocation: Int) { + val (animDuration, delay) = getAnimationParams(previousLocation, desiredLocation) + animator.apply { + duration = animDuration + startDelay = delay + } + + } + + private fun getAnimationParams(previousLocation: Int, desiredLocation: Int): Pair { var animDuration = 200L var delay = 0L if (previousLocation == LOCATION_LOCKSCREEN && desiredLocation == LOCATION_QQS) { @@ -206,11 +220,7 @@ class MediaHierarchyManager @Inject constructor( } else if (previousLocation == LOCATION_QQS && desiredLocation == LOCATION_LOCKSCREEN) { animDuration = StackStateAnimator.ANIMATION_DURATION_APPEAR_DISAPPEAR.toLong() } - animator.apply { - duration = animDuration - startDelay = delay - } - + return animDuration to delay } private fun applyTargetStateIfNotAnimating() { @@ -290,7 +300,7 @@ class MediaHierarchyManager @Inject constructor( boundsOnScreen.bottom) } currentState = state.copy() - mediaViewManager.applyState(currentState) + mediaViewManager.setCurrentState(currentState) } private fun updateHostAttachment() { diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt index 12e2743c1333a..80d00e2d06498 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt @@ -6,6 +6,8 @@ import android.view.View import android.view.View.OnAttachStateChangeListener import android.view.ViewGroup import com.android.systemui.media.MediaHierarchyManager.MediaLocation +import com.android.systemui.util.animation.MeasurementInput +import com.android.systemui.util.animation.MeasurementInputData import javax.inject.Inject class MediaHost @Inject constructor( @@ -86,6 +88,7 @@ class MediaHost @Inject constructor( } class MediaHostState @Inject constructor() : MediaState { + var measurementInput: MediaMeasurementInput? = null override var expansion: Float = 0.0f override var showsOnlyActiveMedia: Boolean = false override val boundsOnScreen: Rect = Rect() @@ -95,6 +98,7 @@ class MediaHost @Inject constructor( mediaHostState.expansion = expansion mediaHostState.showsOnlyActiveMedia = showsOnlyActiveMedia mediaHostState.boundsOnScreen.set(boundsOnScreen) + mediaHostState.measurementInput = measurementInput return mediaHostState } @@ -111,8 +115,20 @@ class MediaHost @Inject constructor( other.boundsOnScreen.bottom.toFloat(), amount).toInt() result.boundsOnScreen.set(left, top, right, bottom) result.showsOnlyActiveMedia = other.showsOnlyActiveMedia || showsOnlyActiveMedia + if (amount > 0.0f) { + if (other is MediaHostState) { + result.measurementInput = other.measurementInput + } + } else { + result.measurementInput + } return result } + + override fun getMeasuringInput(input: MeasurementInput): MediaMeasurementInput { + measurementInput = MediaMeasurementInput(input, expansion) + return measurementInput as MediaMeasurementInput + } } } @@ -122,4 +138,20 @@ interface MediaState { val boundsOnScreen: Rect fun copy() : MediaState fun interpolate(other: MediaState, amount: Float) : MediaState -} \ No newline at end of file + fun getMeasuringInput(input: MeasurementInput): MediaMeasurementInput +} +/** + * The measurement input for a Media View + */ +data class MediaMeasurementInput( + private val viewInput: MeasurementInput, + val expansion: Float) : MeasurementInput by viewInput { + + override fun sameAs(input: MeasurementInput?): Boolean { + if (!(input is MediaMeasurementInput)) { + return false + } + return width == input.width && expansion == input.expansion + } +} + diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaMeasurementManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaMeasurementManager.kt new file mode 100644 index 0000000000000..4bbf5eb9f0dcb --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/MediaMeasurementManager.kt @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2020 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 + */ + +package com.android.systemui.media + +import com.android.systemui.util.animation.BaseMeasurementCache +import com.android.systemui.util.animation.GuaranteedMeasurementCache +import com.android.systemui.util.animation.MeasurementCache +import com.android.systemui.util.animation.MeasurementInput +import com.android.systemui.util.animation.MeasurementOutput +import javax.inject.Inject +import javax.inject.Singleton + +/** + * A class responsible creating measurement caches for media hosts which also coordinates with + * the view manager to obtain sizes for unknown measurement inputs. + */ +@Singleton +class MediaMeasurementManager @Inject constructor( + private val mediaViewManager: MediaViewManager +) { + private val baseCache: MeasurementCache + + init { + baseCache = BaseMeasurementCache() + } + + private fun provideMeasurement(input: MediaMeasurementInput) : MeasurementOutput? { + return mediaViewManager.obtainMeasurement(input) + } + + /** + * Obtain a guaranteed measurement cache for a host view. The measurement cache makes sure that + * requesting any size from the cache will always return the correct value. + */ + fun obtainCache(host: MediaState): GuaranteedMeasurementCache { + val remapper = { input: MeasurementInput -> + host.getMeasuringInput(input) + } + val provider = { input: MeasurementInput -> + provideMeasurement(input as MediaMeasurementInput) + } + return GuaranteedMeasurementCache(baseCache, remapper, provider) + } +} + diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt index 636e4e46a1b0c..6dd978716a9e8 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt @@ -4,7 +4,6 @@ import android.content.Context import android.view.LayoutInflater import android.view.ViewGroup import android.widget.LinearLayout - import com.android.settingslib.bluetooth.LocalBluetoothManager import com.android.settingslib.media.InfoMediaManager import com.android.settingslib.media.LocalMediaManager @@ -13,7 +12,8 @@ import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.plugins.ActivityStarter import com.android.systemui.statusbar.notification.VisualStabilityManager -import com.android.systemui.util.animation.UniqueObjectHost +import com.android.systemui.util.animation.MeasurementOutput +import com.android.systemui.util.animation.UniqueObjectHostView import com.android.systemui.util.concurrency.DelayableExecutor import java.util.concurrent.Executor import javax.inject.Inject @@ -33,12 +33,14 @@ class MediaViewManager @Inject constructor( private val activityStarter: ActivityStarter, mediaManager: MediaDataManager ) { + private var desiredState: MediaHost.MediaHostState? = null + private var currentState: MediaState? = null val mediaCarousel: ViewGroup private val mediaContent: ViewGroup private val mediaPlayers: MutableMap = mutableMapOf() private val visualStabilityCallback = ::reorderAllPlayers - private var viewsExpanded = true + private var currentlyExpanded = true set(value) { if (field != value) { field = value @@ -68,7 +70,7 @@ class MediaViewManager @Inject constructor( private fun inflateMediaCarousel(): ViewGroup { return LayoutInflater.from(context).inflate( - R.layout.media_carousel, UniqueObjectHost(context), false) as ViewGroup + R.layout.media_carousel, UniqueObjectHostView(context), false) as ViewGroup } private fun reorderAllPlayers() { @@ -98,7 +100,7 @@ class MediaViewManager @Inject constructor( val lp = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) existingPlayer.view.setLayoutParams(lp) - existingPlayer.setListening(viewsExpanded) + existingPlayer.setListening(currentlyExpanded) if (existingPlayer.isPlaying) { mediaContent.addView(existingPlayer.view, 0) } else { @@ -132,32 +134,48 @@ class MediaViewManager @Inject constructor( } - fun applyState(state: MediaState) { + fun setCurrentState(state: MediaState) { + currentState = state + currentlyExpanded = state.expansion > 0 for (mediaPlayer in mediaPlayers.values) { val view = mediaPlayer.view view.progress = state.expansion } - viewsExpanded = state.expansion > 0; } /** * @param targetState the target state we're transitioning to * @param animate should this be animated */ - fun performTransition(targetState: MediaState?, animate: Boolean, duration: Long, - startDelay: Long) { - if (targetState == null) { - return + fun onDesiredStateChanged(targetState: MediaState?, animate: Boolean, duration: Long, + startDelay: Long) { + if (targetState is MediaHost.MediaHostState) { + // This is a hosting view, let's remeasure our players + desiredState = targetState + val measurementInput = targetState.measurementInput + for (mediaPlayer in mediaPlayers.values) { + mediaPlayer.remeasure(measurementInput, animate, duration, startDelay) + } } - val newWidth = targetState.boundsOnScreen.width() - val newHeight = targetState.boundsOnScreen.height() - remeasureViews(newWidth, newHeight, animate, duration, startDelay) } - private fun remeasureViews(newWidth: Int, newHeight: Int, animate: Boolean, duration: Long, - startDelay: Long) { - for (mediaPlayer in mediaPlayers.values) { - mediaPlayer.setDimension(newWidth, newHeight, animate, duration, startDelay) + /** + * Get a measurement for the given input state. This measures the first player and returns + * its bounds as if it were measured with the given measurement dimensions + */ + fun obtainMeasurement(input: MediaMeasurementInput) : MeasurementOutput? { + val firstPlayer = mediaPlayers.values.firstOrNull() ?: return null + // Let's measure the size of the first player and return its height + val previousProgress = firstPlayer.view.progress + firstPlayer.view.progress = input.expansion + firstPlayer.remeasure(input, false /* animate */, 0, 0) + val result = MeasurementOutput(firstPlayer.view.measuredWidth, + firstPlayer.view.measuredHeight) + firstPlayer.view.progress = previousProgress + if (desiredState != null) { + // remeasure it to the old size again! + firstPlayer.remeasure(desiredState!!.measurementInput, false, 0, 0) } + return result } } \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/util/animation/MeasurementCache.kt b/packages/SystemUI/src/com/android/systemui/util/animation/MeasurementCache.kt new file mode 100644 index 0000000000000..2be698b4e796f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/animation/MeasurementCache.kt @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2020 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 + */ + +package com.android.systemui.util.animation + +/** + * A class responsible for caching view Measurements which guarantees that we always obtain a value + */ +class GuaranteedMeasurementCache constructor( + private val baseCache : MeasurementCache, + private val inputMapper: (MeasurementInput) -> MeasurementInput, + private val measurementProvider: (MeasurementInput) -> MeasurementOutput? +) : MeasurementCache { + + override fun obtainMeasurement(input: MeasurementInput) : MeasurementOutput { + val mappedInput = inputMapper.invoke(input) + if (!baseCache.contains(mappedInput)) { + var measurement = measurementProvider.invoke(mappedInput) + if (measurement != null) { + // Only cache measurings that actually have a size + baseCache.putMeasurement(mappedInput, measurement) + } else { + measurement = MeasurementOutput(0, 0) + } + return measurement + } else { + return baseCache.obtainMeasurement(mappedInput) + } + } + + override fun contains(input: MeasurementInput): Boolean { + return baseCache.contains(inputMapper.invoke(input)) + } + + override fun putMeasurement(input: MeasurementInput, output: MeasurementOutput) { + if (output.measuredWidth == 0 || output.measuredHeight == 0) { + // Only cache measurings that actually have a size + return; + } + val remappedInput = inputMapper.invoke(input) + baseCache.putMeasurement(remappedInput, output) + } +} + +/** + * A base implementation class responsible for caching view Measurements + */ +class BaseMeasurementCache : MeasurementCache { + private val dataCache: MutableMap = mutableMapOf() + + override fun obtainMeasurement(input: MeasurementInput) : MeasurementOutput { + val measurementOutput = dataCache[input] + if (measurementOutput == null) { + return MeasurementOutput(0, 0) + } else { + return measurementOutput + } + } + + override fun contains(input: MeasurementInput) : Boolean { + return dataCache[input] != null + } + + override fun putMeasurement(input: MeasurementInput, output: MeasurementOutput) { + dataCache[input] = output + } +} + +interface MeasurementCache { + fun obtainMeasurement(input: MeasurementInput) : MeasurementOutput + fun contains(input: MeasurementInput) : Boolean + fun putMeasurement(input: MeasurementInput, output: MeasurementOutput) +} + diff --git a/packages/SystemUI/src/com/android/systemui/util/animation/UniqueObjectHost.kt b/packages/SystemUI/src/com/android/systemui/util/animation/UniqueObjectHost.kt deleted file mode 100644 index 63fc6c2481fbc..0000000000000 --- a/packages/SystemUI/src/com/android/systemui/util/animation/UniqueObjectHost.kt +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (C) 2020 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 - */ - -package com.android.systemui.util.animation - -import android.content.Context -import android.widget.FrameLayout - -/** - * A special view that hosts a unique object, which only exists once, but can transition between - * different hosts. If the view currently hosts the unique object, it's measuring it normally, - * but if it's not attached, it will obtain the size by requesting a measure. - */ -class UniqueObjectHost(context: Context) : FrameLayout(context) { - private var cachedHeight: Int = 0 - private var cachedWidth: Int = 0 - override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec) - if (isCurrentHost()) { - cachedHeight = measuredHeight - cachedWidth = measuredWidth - } else { - setMeasuredDimension(cachedWidth, cachedHeight) - } - } - - private fun isCurrentHost() = childCount != 0 -} diff --git a/packages/SystemUI/src/com/android/systemui/util/animation/UniqueObjectHostView.kt b/packages/SystemUI/src/com/android/systemui/util/animation/UniqueObjectHostView.kt new file mode 100644 index 0000000000000..346dfa50b9603 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/animation/UniqueObjectHostView.kt @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2020 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 + */ + +package com.android.systemui.util.animation + +import android.annotation.SuppressLint +import android.content.Context +import android.view.View +import android.widget.FrameLayout + +/** + * A special view that hosts a unique object, which only exists once, but can transition between + * different hosts. If the view currently hosts the unique object, it's measuring it normally, + * but if it's not attached, it will obtain the size by requesting a measure. + */ +class UniqueObjectHostView( + context: Context +) : FrameLayout(context) { + lateinit var measurementCache : GuaranteedMeasurementCache + + @SuppressLint("DrawAllocation") + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + if (isCurrentHost()) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + } + val paddingHorizontal = paddingStart + paddingEnd + val paddingVertical = paddingTop + paddingBottom + val width = MeasureSpec.getSize(widthMeasureSpec) - paddingHorizontal + val widthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.getMode(widthMeasureSpec)) + val height = MeasureSpec.getSize(heightMeasureSpec) - paddingVertical + val heightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.getMode(heightMeasureSpec)) + val measurementInput = MeasurementInputData(widthSpec, heightSpec) + if (!isCurrentHost() || !measurementCache.contains(measurementInput)) { + // We're not currently the host, let's get the dimension from our cache (this might + // perform a measuring if the cache doesn't have it yet + val (cachedWidth, cachedHeight) = measurementCache.obtainMeasurement(measurementInput) + setMeasuredDimension(cachedWidth + paddingHorizontal, cachedHeight + paddingVertical) + } else { + // Let's update what we have in the cache if it's not present yet + val child = getChildAt(0)!! + val output = MeasurementOutput(child.measuredWidth, child.measuredHeight) + measurementCache.putMeasurement(measurementInput, output) + } + } + + private fun isCurrentHost() = childCount != 0 +} + +/** + * A basic view measurement input + */ +interface MeasurementInput { + fun sameAs(input: MeasurementInput?): Boolean { + return equals(input) + } + val width : Int + get() { + return View.MeasureSpec.getSize(widthMeasureSpec) + } + val height : Int + get() { + return View.MeasureSpec.getSize(heightMeasureSpec) + } + var widthMeasureSpec: Int + var heightMeasureSpec: Int +} + +/** + * The output of a view measurement + */ +data class MeasurementOutput( + val measuredWidth: Int, + val measuredHeight: Int +) + +/** + * The data object holding a basic view measurement input + */ +data class MeasurementInputData( + override var widthMeasureSpec: Int, + override var heightMeasureSpec: Int +) : MeasurementInput diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java index 8f2011e229440..b8ecd3ff26534 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java @@ -45,7 +45,6 @@ import androidx.test.filters.SmallTest; import com.android.systemui.ActivityStarterDelegate; import com.android.systemui.SysuiTestCase; import com.android.systemui.media.KeyguardMediaController; -import com.android.systemui.media.MediaHierarchyManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.notification.NotificationSectionsFeatureManager; @@ -54,7 +53,6 @@ import com.android.systemui.statusbar.notification.row.ActivatableNotificationVi import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.dagger.NotificationRowComponent; import com.android.systemui.statusbar.policy.ConfigurationController; -import com.android.systemui.util.animation.UniqueObjectHost; import org.junit.Before; import org.junit.Rule; From 2d7be5f40f79942b9e5cf47bbc28912670c0399d Mon Sep 17 00:00:00 2001 From: Selim Cinek Date: Fri, 1 May 2020 13:16:01 -0700 Subject: [PATCH 11/14] Made the animation logic more stable We're now using the most recent state of the last attached location for the animation start, since otherwise this could be flickering. The animation could detoriate if the view wasn't attached to the screen anymore as a result, which is fixed as well. Bug: 154137987 Test: add media notification, observe Change-Id: I1a6dfa651d676b3d620577fbd6c9e6a5d833dc0c --- .../systemui/media/KeyguardMediaController.kt | 31 +++++++++++++----- .../systemui/media/MediaHierarchyManager.kt | 32 +++++++++++++++---- .../com/android/systemui/media/MediaHost.kt | 5 ++- .../notification/stack/MediaHeaderView.java | 8 ++--- .../stack/NotificationSectionsManager.java | 3 +- .../util/animation/UniqueObjectHostView.kt | 2 +- .../NotificationSectionsManagerTest.java | 32 ------------------- 7 files changed, 58 insertions(+), 55 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt b/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt index b87a1db67ebe7..524c6955ba4a0 100644 --- a/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt @@ -17,6 +17,9 @@ package com.android.systemui.media import android.view.View +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.stack.MediaHeaderView import com.android.systemui.statusbar.phone.KeyguardBypassController import javax.inject.Inject @@ -29,8 +32,17 @@ import javax.inject.Singleton @Singleton class KeyguardMediaController @Inject constructor( private val mediaHost: MediaHost, - private val bypassController: KeyguardBypassController + private val bypassController: KeyguardBypassController, + private val statusBarStateController: SysuiStatusBarStateController ) { + + init { + statusBarStateController.addCallback(object : StatusBarStateController.StateListener { + override fun onStateChanged(newState: Int) { + updateVisibility() + } + }) + } private var view: MediaHeaderView? = null /** @@ -38,18 +50,21 @@ class KeyguardMediaController @Inject constructor( */ fun attach(mediaView: MediaHeaderView) { view = mediaView - mediaHost.visibleChangedListener = ::updateVisibility + // First let's set the desired state that we want for this host + mediaHost.visibleChangedListener = { updateVisibility() } mediaHost.expansion = 0.0f mediaHost.showsOnlyActiveMedia = true + + // Let's now initialize this view, which also creates the host view for us. mediaHost.init(MediaHierarchyManager.LOCATION_LOCKSCREEN) - mediaView.setMediaHost(mediaHost.hostView) - /** - * Attach this controller to a media view, initializing its state - */ + mediaView.setContentView(mediaHost.hostView) } - private fun updateVisibility(isVisible: Boolean) { - val shouldBeVisible = isVisible && !bypassController.bypassEnabled + private fun updateVisibility() { + val shouldBeVisible = mediaHost.visible + && !bypassController.bypassEnabled + && (statusBarStateController.state == StatusBarState.KEYGUARD || + statusBarStateController.state == StatusBarState.FULLSCREEN_USER_SWITCHER) view?.visibility = if (shouldBeVisible) View.VISIBLE else View.GONE } } diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt index 26a61046c6df9..391b1124cd090 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt @@ -53,6 +53,7 @@ class MediaHierarchyManager @Inject constructor( private val mediaCarousel get() = mediaViewManager.mediaCarousel private var animationStartState: MediaState? = null + private var statusbarState: Int = statusBarStateController.state private var animator = ValueAnimator.ofFloat(0.0f, 1.0f).apply { interpolator = Interpolators.FAST_OUT_SLOW_IN addUpdateListener { @@ -96,9 +97,16 @@ class MediaHierarchyManager @Inject constructor( init { statusBarStateController.addCallback(object : StatusBarStateController.StateListener { - override fun onStateChanged(newState: Int) { + override fun onStatePreChange(oldState: Int, newState: Int) { + // We're updating the location before the state change happens, since we want the + // location of the previous state to still be up to date when the animation starts + statusbarState = newState updateDesiredLocation() } + + override fun onStateChanged(newState: Int) { + updateTargetState() + } }) } @@ -168,14 +176,24 @@ class MediaHierarchyManager @Inject constructor( val previousHost = getHost(previousLocation) if (currentHost == null || previousHost == null) { cancelAnimationAndApplyDesiredState() + return } updateTargetState() if (isCurrentlyInGuidedTransformation()) { applyTargetStateIfNotAnimating() } else if (animate) { animator.cancel() - // Let's animate to the new position, starting from the current position - animationStartState = currentState.copy() + if (currentAttachmentLocation == IN_OVERLAY + || !previousHost.hostView.isAttachedToWindow) { + // Let's animate to the new position, starting from the current position + // We also go in here in case the view was detached, since the bounds wouldn't + // be correct anymore + animationStartState = currentState.copy() + } else { + // otherwise, let's take the freshest state, since the current one could + // be outdated + animationStartState = previousHost.currentState.copy() + } adjustAnimatorForTransition(desiredLocation, previousLocation) animator.start() } else { @@ -190,7 +208,7 @@ class MediaHierarchyManager @Inject constructor( if (currentLocation == LOCATION_QQS && previousLocation == LOCATION_LOCKSCREEN && (statusBarStateController.leaveOpenOnKeyguardHide() - || statusBarStateController.state == StatusBarState.SHADE_LOCKED)) { + || statusbarState == 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 @@ -212,7 +230,7 @@ class MediaHierarchyManager @Inject constructor( 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 + if (statusbarState == StatusBarState.SHADE && keyguardStateController.isKeyguardFadingAway) { delay = keyguardStateController.keyguardFadingAwayDelay } @@ -330,8 +348,8 @@ class MediaHierarchyManager @Inject constructor( @MediaLocation private fun calculateLocation() : Int { val onLockscreen = (!bypassController.bypassEnabled - && (statusBarStateController.state == StatusBarState.KEYGUARD - || statusBarStateController.state == StatusBarState.FULLSCREEN_USER_SWITCHER)) + && (statusbarState == StatusBarState.KEYGUARD + || statusbarState == StatusBarState.FULLSCREEN_USER_SWITCHER)) return when { qsExpansion > 0.0f && !onLockscreen -> LOCATION_QS qsExpansion > 0.4f && onLockscreen -> LOCATION_QS diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt index 80d00e2d06498..e1500ac09dc96 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt @@ -59,7 +59,10 @@ class MediaHost @Inject constructor( } /** - * Initialize this MediaObject and create a host view + * Initialize this MediaObject and create a host view. + * + * @param location the location this host name has. Used to identify the host during + * transitions. */ fun init(@MediaLocation location: Int) { this.location = location; 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 65594efd064c9..3ac322fec0716 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 @@ -53,10 +53,10 @@ public class MediaHeaderView extends ActivatableNotificationView { setTintColor(color); } - public void setMediaHost(ViewGroup mediaHost) { - mContentView = mediaHost; - addView(mediaHost); - ViewGroup.LayoutParams layoutParams = mediaHost.getLayoutParams(); + public void setContentView(ViewGroup contentView) { + mContentView = contentView; + addView(contentView); + ViewGroup.LayoutParams layoutParams = contentView.getLayoutParams(); layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT; layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.java index 9f8e04468116f..2a257f6198d7b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.java @@ -266,7 +266,6 @@ public class NotificationSectionsManager implements StackScrollAlgorithm.Section final boolean showHeaders = mStatusBarStateController.getState() != StatusBarState.KEYGUARD; final boolean usingPeopleFiltering = mSectionsFeatureManager.isFilteringEnabled(); - final boolean isKeyguard = mStatusBarStateController.getState() == StatusBarState.KEYGUARD; final boolean usingMediaControls = mSectionsFeatureManager.isMediaControlsEnabled(); boolean peopleNotifsPresent = false; @@ -274,7 +273,7 @@ public class NotificationSectionsManager implements StackScrollAlgorithm.Section int currentMediaControlsIdx = -1; // Currently, just putting media controls in the front and incrementing the position based // on the number of heads-up notifs. - int mediaControlsTarget = isKeyguard && usingMediaControls ? 0 : -1; + int mediaControlsTarget = usingMediaControls ? 0 : -1; int currentIncomingHeaderIdx = -1; int incomingHeaderTarget = -1; int currentPeopleHeaderIdx = -1; diff --git a/packages/SystemUI/src/com/android/systemui/util/animation/UniqueObjectHostView.kt b/packages/SystemUI/src/com/android/systemui/util/animation/UniqueObjectHostView.kt index 346dfa50b9603..9c993aae443e1 100644 --- a/packages/SystemUI/src/com/android/systemui/util/animation/UniqueObjectHostView.kt +++ b/packages/SystemUI/src/com/android/systemui/util/animation/UniqueObjectHostView.kt @@ -49,7 +49,7 @@ class UniqueObjectHostView( val (cachedWidth, cachedHeight) = measurementCache.obtainMeasurement(measurementInput) setMeasuredDimension(cachedWidth + paddingHorizontal, cachedHeight + paddingVertical) } else { - // Let's update what we have in the cache if it's not present yet + // Let's update what we have in the cache if it's present val child = getChildAt(0)!! val output = MeasurementOutput(child.measuredWidth, child.measuredHeight) measurementCache.putMeasurement(measurementInput, output) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java index b8ecd3ff26534..f4b5a5b7f3cf8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java @@ -367,38 +367,6 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { verify(mNssl).addView(mSectionsManager.getMediaControlsView(), 1); } - @Test - public void testMediaControls_RemoveWhenExitKeyguard() { - enableMediaControls(); - - // GIVEN a stack with media controls - setStackState(ChildType.MEDIA_CONTROLS, ChildType.ALERTING, ChildType.GENTLE_HEADER, - ChildType.GENTLE); - - // WHEN we leave the keyguard - when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE); - mSectionsManager.updateSectionBoundaries(); - - // Then the media controls is removed - verify(mNssl).removeView(mSectionsManager.getMediaControlsView()); - } - - @Test - public void testMediaControls_RemoveWhenPullDownShade() { - enableMediaControls(); - - // GIVEN a stack with media controls - setStackState(ChildType.MEDIA_CONTROLS, ChildType.ALERTING, ChildType.GENTLE_HEADER, - ChildType.GENTLE); - - // WHEN we pull down the shade on the keyguard - when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE_LOCKED); - mSectionsManager.updateSectionBoundaries(); - - // Then the media controls is removed - verify(mNssl).removeView(mSectionsManager.getMediaControlsView()); - } - private void enablePeopleFiltering() { when(mSectionsFeatureManager.isFilteringEnabled()).thenReturn(true); when(mSectionsFeatureManager.getNumberOfBuckets()).thenReturn(4); From b28ec0a32fec8b3d8e86b15d057f95ebd5e0e937 Mon Sep 17 00:00:00 2001 From: Selim Cinek Date: Fri, 1 May 2020 15:07:42 -0700 Subject: [PATCH 12/14] Fixing some issues where view was too short when created The view was always created too small because we didn't set the constraints in the beginning Test: add new media notification, not clipped off Bug: 154137987 Change-Id: Ifb9c95ba41be7392ec4b61e2b801b7420b811d3a --- .../systemui/media/MediaControlPanel.java | 20 ++++++++------ .../systemui/media/MediaHierarchyManager.kt | 14 +++++++++- .../systemui/media/MediaViewManager.kt | 26 ++++++++++++++++--- .../util/animation/UniqueObjectHostView.kt | 15 ++++++----- 4 files changed, 56 insertions(+), 19 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java index 3d638dd9e211e..2a9ba8395ee75 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java +++ b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java @@ -396,6 +396,8 @@ public class MediaControlPanel { // TODO: b/156036025 bring back media guts makeActive(); + mMediaNotifView.updateState(R.id.collapsed, collapsedSet); + mMediaNotifView.updateState(R.id.expanded, expandedSet); } private Drawable createRoundedBitmap(Icon icon) { @@ -753,16 +755,18 @@ public class MediaControlPanel { } } - public MediaMeasurementInput getLastMeasureInput() { - return mLastMeasureInput; - } - private void remeasureInternal(MediaMeasurementInput input) { - ConstraintSet expandedSet = mMediaNotifView.getConstraintSet(R.id.expanded); - ConstraintSet collapsedSet = mMediaNotifView.getConstraintSet(R.id.collapsed); int width = input.getWidth(); - collapsedSet.setGuidelineBegin(R.id.view_width, width); - expandedSet.setGuidelineBegin(R.id.view_width, width); + setPlayerWidth(width); mMediaNotifView.measure(input.getWidthMeasureSpec(), input.getHeightMeasureSpec()); } + + public void setPlayerWidth(int width) { + ConstraintSet expandedSet = mMediaNotifView.getConstraintSet(R.id.expanded); + ConstraintSet collapsedSet = mMediaNotifView.getConstraintSet(R.id.collapsed); + collapsedSet.setGuidelineBegin(R.id.view_width, width); + expandedSet.setGuidelineBegin(R.id.view_width, width); + mMediaNotifView.updateState(R.id.collapsed, collapsedSet); + mMediaNotifView.updateState(R.id.expanded, expandedSet); + } } diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt index 391b1124cd090..b4c0c334773e8 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt @@ -132,9 +132,21 @@ class MediaHierarchyManager @Inject constructor( return viewHost } - private fun createUniqueObjectHost(host: MediaState): UniqueObjectHostView { + private fun createUniqueObjectHost(host: MediaHost): UniqueObjectHostView { val viewHost = UniqueObjectHostView(context) viewHost.measurementCache = mediaMeasurementProvider.obtainCache(host) + viewHost.firstMeasureListener = { input -> + if (host.location == currentAttachmentLocation) { + // The first measurement of the attached view is happening, Let's make + // sure the player width is updated + val measuringInput = host.getMeasuringInput(input) + mediaViewManager.remeasureAllPlayers( + measuringInput, + animate = false, + duration = 0, + startDelay = 0) + } + } viewHost.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener { override fun onViewAttachedToWindow(p0: View?) { diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt index 6dd978716a9e8..a364e6b73e60a 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt @@ -63,6 +63,7 @@ class MediaViewManager @Inject constructor( removed?.apply { mediaContent.removeView(removed.view) removed.onDestroy() + updateMediaPaddings() } } }) @@ -106,8 +107,9 @@ class MediaViewManager @Inject constructor( } else { mediaContent.addView(existingPlayer.view) } + updatePlayerToCurrentState(existingPlayer) } else if (existingPlayer.isPlaying && - mediaContent.indexOfChild(existingPlayer.view) != 0) { + mediaContent.indexOfChild(existingPlayer.view) != 0) { if (visualStabilityManager.isReorderingAllowed) { mediaContent.removeView(existingPlayer.view) mediaContent.addView(existingPlayer.view, 0) @@ -116,9 +118,20 @@ class MediaViewManager @Inject constructor( } } existingPlayer.bind(data) + // Resetting the progress to make sure it's taken into account for the latest + // motion model + existingPlayer.view.progress = currentState?.expansion ?: 0.0f updateMediaPaddings() } + private fun updatePlayerToCurrentState(existingPlayer: MediaControlPanel) { + if (desiredState != null && desiredState!!.measurementInput != null) { + // make sure the player width is set to the current state + val measurementInput = desiredState!!.measurementInput!! + existingPlayer.setPlayerWidth(measurementInput.width) + } + } + private fun updateMediaPaddings() { val padding = context.resources.getDimensionPixelSize(R.dimen.qs_media_padding) val childCount = mediaContent.childCount @@ -153,9 +166,14 @@ class MediaViewManager @Inject constructor( // This is a hosting view, let's remeasure our players desiredState = targetState val measurementInput = targetState.measurementInput - for (mediaPlayer in mediaPlayers.values) { - mediaPlayer.remeasure(measurementInput, animate, duration, startDelay) - } + remeasureAllPlayers(measurementInput, animate, duration, startDelay) + } + } + + fun remeasureAllPlayers(measurementInput: MediaMeasurementInput?, + animate: Boolean, duration: Long, startDelay: Long) { + for (mediaPlayer in mediaPlayers.values) { + mediaPlayer.remeasure(measurementInput, animate, duration, startDelay) } } diff --git a/packages/SystemUI/src/com/android/systemui/util/animation/UniqueObjectHostView.kt b/packages/SystemUI/src/com/android/systemui/util/animation/UniqueObjectHostView.kt index 9c993aae443e1..376d09110d20e 100644 --- a/packages/SystemUI/src/com/android/systemui/util/animation/UniqueObjectHostView.kt +++ b/packages/SystemUI/src/com/android/systemui/util/animation/UniqueObjectHostView.kt @@ -30,12 +30,10 @@ class UniqueObjectHostView( context: Context ) : FrameLayout(context) { lateinit var measurementCache : GuaranteedMeasurementCache + var firstMeasureListener: ((MeasurementInput) -> Unit)? = null @SuppressLint("DrawAllocation") override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { - if (isCurrentHost()) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec) - } val paddingHorizontal = paddingStart + paddingEnd val paddingVertical = paddingTop + paddingBottom val width = MeasureSpec.getSize(widthMeasureSpec) - paddingHorizontal @@ -43,13 +41,18 @@ class UniqueObjectHostView( val height = MeasureSpec.getSize(heightMeasureSpec) - paddingVertical val heightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.getMode(heightMeasureSpec)) val measurementInput = MeasurementInputData(widthSpec, heightSpec) - if (!isCurrentHost() || !measurementCache.contains(measurementInput)) { + firstMeasureListener?.apply { + invoke(measurementInput) + firstMeasureListener = null + } + if (!isCurrentHost()) { // We're not currently the host, let's get the dimension from our cache (this might - // perform a measuring if the cache doesn't have it yet + // perform a measuring if the cache doesn't have it yet) val (cachedWidth, cachedHeight) = measurementCache.obtainMeasurement(measurementInput) setMeasuredDimension(cachedWidth + paddingHorizontal, cachedHeight + paddingVertical) } else { - // Let's update what we have in the cache if it's present + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + // Let's update our cache val child = getChildAt(0)!! val output = MeasurementOutput(child.measuredWidth, child.measuredHeight) measurementCache.putMeasurement(measurementInput, output) From 8081f0940f727faf35394b71cdf602caf55b0f21 Mon Sep 17 00:00:00 2001 From: Selim Cinek Date: Fri, 1 May 2020 21:11:13 -0700 Subject: [PATCH 13/14] Fixed an issue where GONE Views would show during transitions Because we're clipping children and MotionLayout doesn't hide them when collapsed, views could flicker onto the screen Test: add media notification, swipe down to shade and back, observe no flickering Bug: 154137987 Change-Id: Ida591706e779215e13c6d3047be7e3b5318b4d19 --- packages/SystemUI/res/xml/media_scene.xml | 2 + .../systemui/media/GoneChildrenHideHelper.kt | 52 +++++++++++++++++++ .../systemui/media/MediaControlPanel.java | 32 +++++++----- 3 files changed, 73 insertions(+), 13 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/media/GoneChildrenHideHelper.kt diff --git a/packages/SystemUI/res/xml/media_scene.xml b/packages/SystemUI/res/xml/media_scene.xml index 6f99d9a675f12..f61b2b096d3c8 100644 --- a/packages/SystemUI/res/xml/media_scene.xml +++ b/packages/SystemUI/res/xml/media_scene.xml @@ -356,6 +356,7 @@ android:id="@+id/media_progress_bar" android:layout_width="0dp" android:layout_height="wrap_content" + android:alpha="0.0" app:layout_constraintTop_toBottomOf="@id/album_art" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="@id/view_width" @@ -364,6 +365,7 @@ { final Intent intent = new Intent() @@ -344,9 +349,6 @@ public class MediaControlPanel { Log.d(TAG, "PlaybackInfo was null. Defaulting to local playback."); mIsRemotePlayback = false; } - - ConstraintSet expandedSet = mMediaNotifView.getConstraintSet(R.id.expanded); - ConstraintSet collapsedSet = mMediaNotifView.getConstraintSet(R.id.collapsed); List actionsWhenCollapsed = data.getActionsToShowInCompact(); // Media controls int i = 0; @@ -376,15 +378,14 @@ public class MediaControlPanel { }); boolean visibleInCompat = actionsWhenCollapsed.contains(i); updateKeyFrameVisibility(actionId, visibleInCompat); - collapsedSet.setVisibility(actionId, - visibleInCompat ? ConstraintSet.VISIBLE : ConstraintSet.GONE); - expandedSet.setVisibility(actionId, ConstraintSet.VISIBLE); + setVisibleAndAlpha(collapsedSet, actionId, visibleInCompat); + setVisibleAndAlpha(expandedSet, actionId, true /*visible */); } // Hide any unused buttons for (; i < ACTION_IDS.length; i++) { - expandedSet.setVisibility(ACTION_IDS[i], ConstraintSet.GONE); - collapsedSet.setVisibility(ACTION_IDS[i], ConstraintSet.GONE); + setVisibleAndAlpha(expandedSet, ACTION_IDS[i], false /*visible */); + setVisibleAndAlpha(collapsedSet, ACTION_IDS[i], false /*visible */); } // Seek Bar @@ -604,8 +605,8 @@ public class MediaControlPanel { ConstraintSet expandedSet = mMediaNotifView.getConstraintSet(R.id.expanded); ConstraintSet collapsedSet = mMediaNotifView.getConstraintSet(R.id.collapsed); for (int i = 1; i < ACTION_IDS.length; i++) { - expandedSet.setVisibility(ACTION_IDS[i], ConstraintSet.GONE); - collapsedSet.setVisibility(ACTION_IDS[i], ConstraintSet.GONE); + setVisibleAndAlpha(expandedSet, ACTION_IDS[i], false /*visible */); + setVisibleAndAlpha(collapsedSet, ACTION_IDS[i], false /*visible */); } // Add a restart button @@ -631,8 +632,8 @@ public class MediaControlPanel { }); btn.setImageDrawable(mContext.getResources().getDrawable(R.drawable.lb_ic_play)); btn.setImageTintList(ColorStateList.valueOf(mForegroundColor)); - expandedSet.setVisibility(ACTION_IDS[0], ConstraintSet.VISIBLE); - collapsedSet.setVisibility(ACTION_IDS[0], ConstraintSet.VISIBLE); + setVisibleAndAlpha(expandedSet, ACTION_IDS[0], true /*visible */); + setVisibleAndAlpha(collapsedSet, ACTION_IDS[0], true /*visible */); mSeekBarViewModel.clearController(); // TODO: fix guts @@ -647,6 +648,11 @@ public class MediaControlPanel { }); } + private void setVisibleAndAlpha(ConstraintSet set, int actionId, boolean visible) { + set.setVisibility(actionId, visible? ConstraintSet.VISIBLE : ConstraintSet.GONE); + set.setAlpha(actionId, visible ? 1.0f : 0.0f); + } + private void makeActive() { Assert.isMainThread(); if (!mIsRegistered) { From f418bb0275c5234a44d5d138f70b1dfad529be7d Mon Sep 17 00:00:00 2001 From: Selim Cinek Date: Mon, 4 May 2020 17:16:58 -0700 Subject: [PATCH 14/14] Handling multiple players better Previously the animation was completely broken with multiple players since they were all laid out at 0 instead of the actual position. Measuring the full layout is a bit too expansive, so we're introducing some workarounds to only measure the players. Test: add multiple players, observe transitions Bug: 154137987 Change-Id: I1c424c980cf3b64f5a9d63ba058aa7e47f6e4156 --- .../SystemUI/res/layout/media_carousel.xml | 4 +- .../systemui/media/MediaControlPanel.java | 40 ++--- .../systemui/media/MediaHierarchyManager.kt | 56 +++++-- .../com/android/systemui/media/MediaHost.kt | 1 - .../systemui/media/MediaViewManager.kt | 141 +++++++++++++++--- .../media/UnboundHorizontalScrollView.kt | 31 ++++ .../util/animation/UniqueObjectHostView.kt | 22 ++- 7 files changed, 227 insertions(+), 68 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/media/UnboundHorizontalScrollView.kt diff --git a/packages/SystemUI/res/layout/media_carousel.xml b/packages/SystemUI/res/layout/media_carousel.xml index f888ca11b5fba..03e74676f03e4 100644 --- a/packages/SystemUI/res/layout/media_carousel.xml +++ b/packages/SystemUI/res/layout/media_carousel.xml @@ -16,7 +16,7 @@ --> - - + diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java index 72282714365b7..60c2ed2fa2be8 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java +++ b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java @@ -47,6 +47,7 @@ import android.widget.SeekBar; import android.widget.TextView; import androidx.annotation.Nullable; +import androidx.annotation.UiThread; import androidx.constraintlayout.motion.widget.Key; import androidx.constraintlayout.motion.widget.KeyAttributes; import androidx.constraintlayout.motion.widget.KeyFrames; @@ -111,7 +112,6 @@ public class MediaControlPanel { private int mAlbumArtSize; private int mAlbumArtRadius; private int mViewWidth; - private MediaMeasurementInput mLastMeasureInput; public static final String MEDIA_PREFERENCES = "media_control_prefs"; public static final String MEDIA_PREFERENCE_KEY = "browser_components"; @@ -188,7 +188,6 @@ public class MediaControlPanel { mActivityStarter = activityStarter; mSeekBarViewModel = new SeekBarViewModel(backgroundExecutor); mSeekBarObserver = new SeekBarObserver(getView()); - // TODO: we should pause this whenever the screen is off / panel is collapsed etc. mSeekBarViewModel.getProgress().observeForever(mSeekBarObserver); SeekBar bar = getView().findViewById(R.id.media_progress_bar); bar.setOnSeekBarChangeListener(mSeekBarViewModel.getSeekBarListener()); @@ -261,7 +260,7 @@ public class MediaControlPanel { // Try to find a browser service component for this app // TODO also check for a media button receiver intended for restarting (b/154127084) // Only check if we haven't tried yet or the session token changed - final String pkgName = mController.getPackageName(); + final String pkgName = data.getPackageName(); if (mServiceComponent == null && !mCheckedForResumption) { Log.d(TAG, "Checking for service component"); PackageManager pm = mContext.getPackageManager(); @@ -301,8 +300,7 @@ public class MediaControlPanel { // App icon ImageView appIcon = mMediaNotifView.requireViewById(R.id.icon); - // TODO: look at iconDrawable - Drawable iconDrawable = data.getAppIcon(); + Drawable iconDrawable = data.getAppIcon().mutate(); iconDrawable.setTint(mForegroundColor); appIcon.setImageDrawable(iconDrawable); @@ -389,7 +387,7 @@ public class MediaControlPanel { } // Seek Bar - final MediaController controller = new MediaController(getContext(), data.getToken()); + final MediaController controller = getController(); mBackgroundExecutor.execute( () -> mSeekBarViewModel.updateController(controller, data.getForegroundColor())); @@ -397,10 +395,13 @@ public class MediaControlPanel { // TODO: b/156036025 bring back media guts makeActive(); + + // Update both constraint sets to regenerate the animation. mMediaNotifView.updateState(R.id.collapsed, collapsedSet); mMediaNotifView.updateState(R.id.expanded, expandedSet); } + @UiThread private Drawable createRoundedBitmap(Icon icon) { if (icon == null) { return null; @@ -746,27 +747,14 @@ public class MediaControlPanel { */ protected void removePlayer() { } - public void remeasure(@Nullable MediaMeasurementInput input, boolean animate, long duration, - long startDelay) { - // Let's remeasure if our width changed. Our height is dependent on the expansion, so we - // won't animate if it changed - if (input != null && !input.sameAs(mLastMeasureInput)) { - mLastMeasureInput = input; - if (animate) { - mLayoutAnimationHelper.animatePendingSizeChange(duration, startDelay); - } - remeasureInternal(input); - mMediaNotifView.layout(0, 0, mMediaNotifView.getMeasuredWidth(), - mMediaNotifView.getMeasuredHeight()); + public void measure(@Nullable MediaMeasurementInput input) { + if (input != null) { + int width = input.getWidth(); + setPlayerWidth(width); + mMediaNotifView.measure(input.getWidthMeasureSpec(), input.getHeightMeasureSpec()); } } - private void remeasureInternal(MediaMeasurementInput input) { - int width = input.getWidth(); - setPlayerWidth(width); - mMediaNotifView.measure(input.getWidthMeasureSpec(), input.getHeightMeasureSpec()); - } - public void setPlayerWidth(int width) { ConstraintSet expandedSet = mMediaNotifView.getConstraintSet(R.id.expanded); ConstraintSet collapsedSet = mMediaNotifView.getConstraintSet(R.id.collapsed); @@ -775,4 +763,8 @@ public class MediaControlPanel { mMediaNotifView.updateState(R.id.collapsed, collapsedSet); mMediaNotifView.updateState(R.id.expanded, expandedSet); } + + public void animatePendingSizeChange(long duration, long startDelay) { + mLayoutAnimationHelper.animatePendingSizeChange(duration, startDelay); + } } diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt index b4c0c334773e8..6b1c520db7b10 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt @@ -48,6 +48,11 @@ class MediaHierarchyManager @Inject constructor( private val mediaViewManager: MediaViewManager, private val mediaMeasurementProvider: MediaMeasurementManager ) { + /** + * The root overlay of the hierarchy. This is where the media notification is attached to + * whenever the view is transitioning from one host to another. It also make sure that the + * view is always in its final state when it is attached to a view host. + */ private var rootOverlay: ViewGroupOverlay? = null private lateinit var currentState: MediaState private val mediaCarousel @@ -79,9 +84,24 @@ class MediaHierarchyManager @Inject constructor( } private var targetState: MediaState? = null private val mediaHosts = arrayOfNulls(LOCATION_LOCKSCREEN + 1) - private var previousLocation = -1 - private var desiredLocation = -1 - private var currentAttachmentLocation = -1 + + /** + * The last location where this view was at before going to the desired location. This is + * useful for guided transitions. + */ + @MediaLocation private var previousLocation = -1 + + /** + * The desired location where the view will be at the end of the transition. + */ + @MediaLocation private var desiredLocation = -1 + + /** + * The current attachment location where the view is currently attached. + * Usually this matches the desired location except for animations whenever a view moves + * to the new desired location, during which it is in [IN_OVERLAY]. + */ + @MediaLocation private var currentAttachmentLocation = -1 var qsExpansion: Float = 0.0f set(value) { @@ -135,16 +155,12 @@ class MediaHierarchyManager @Inject constructor( private fun createUniqueObjectHost(host: MediaHost): UniqueObjectHostView { val viewHost = UniqueObjectHostView(context) viewHost.measurementCache = mediaMeasurementProvider.obtainCache(host) - viewHost.firstMeasureListener = { input -> - if (host.location == currentAttachmentLocation) { - // The first measurement of the attached view is happening, Let's make - // sure the player width is updated + viewHost.onMeasureListener = { input -> + if (host.location == desiredLocation) { + // Measurement of the currently active player is happening, Let's make + // sure the player width is up to date val measuringInput = host.getMeasuringInput(input) - mediaViewManager.remeasureAllPlayers( - measuringInput, - animate = false, - duration = 0, - startDelay = 0) + mediaViewManager.setPlayerWidth(measuringInput.width) } } @@ -162,6 +178,10 @@ class MediaHierarchyManager @Inject constructor( return viewHost } + /** + * Updates the location that the view should be in. If it changes, an animation may be triggered + * going from the old desired location to the new one. + */ private fun updateDesiredLocation() { val desiredLocation = calculateLocation() if (desiredLocation != this.desiredLocation) { @@ -173,7 +193,7 @@ class MediaHierarchyManager @Inject constructor( // Let's perform a transition val animate = shouldAnimateTransition(desiredLocation, previousLocation) val (animDuration, delay) = getAnimationParams(previousLocation, desiredLocation) - mediaViewManager.onDesiredStateChanged(getHost(desiredLocation)?.currentState, + mediaViewManager.onDesiredLocationChanged(getHost(desiredLocation)?.currentState, animate, animDuration, delay) performTransitionToNewLocation(isNewView, animate) } @@ -262,6 +282,9 @@ class MediaHierarchyManager @Inject constructor( } } + /** + * Updates the state that the view wants to be in at the end of the animation. + */ private fun updateTargetState() { if (isCurrentlyInGuidedTransformation()) { val progress = getTransformationProgress() @@ -320,17 +343,17 @@ class MediaHierarchyManager @Inject constructor( } private fun applyState(state: MediaState) { + currentState = state.copy() + mediaViewManager.setCurrentState(currentState) updateHostAttachment() - val boundsOnScreen = state.boundsOnScreen if (currentAttachmentLocation == IN_OVERLAY) { + val boundsOnScreen = state.boundsOnScreen mediaCarousel.setLeftTopRightBottom( boundsOnScreen.left, boundsOnScreen.top, boundsOnScreen.right, boundsOnScreen.bottom) } - currentState = state.copy() - mediaViewManager.setCurrentState(currentState) } private fun updateHostAttachment() { @@ -348,6 +371,7 @@ class MediaHierarchyManager @Inject constructor( rootOverlay!!.add(mediaCarousel) } else { targetHost.addView(mediaCarousel) + mediaViewManager.onViewReattached() } } } diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt index e1500ac09dc96..6e7b6bcb75029 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt @@ -7,7 +7,6 @@ import android.view.View.OnAttachStateChangeListener import android.view.ViewGroup import com.android.systemui.media.MediaHierarchyManager.MediaLocation import com.android.systemui.util.animation.MeasurementInput -import com.android.systemui.util.animation.MeasurementInputData import javax.inject.Inject class MediaHost @Inject constructor( diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt index a364e6b73e60a..49d2d8860a2fa 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt @@ -2,7 +2,9 @@ package com.android.systemui.media import android.content.Context import android.view.LayoutInflater +import android.view.View import android.view.ViewGroup +import android.widget.HorizontalScrollView import android.widget.LinearLayout import com.android.settingslib.bluetooth.LocalBluetoothManager import com.android.settingslib.media.InfoMediaManager @@ -33,12 +35,16 @@ class MediaViewManager @Inject constructor( private val activityStarter: ActivityStarter, mediaManager: MediaDataManager ) { + private var playerWidth: Int = 0 + private var playerWidthPlusPadding: Int = 0 private var desiredState: MediaHost.MediaHostState? = null private var currentState: MediaState? = null - val mediaCarousel: ViewGroup + val mediaCarousel: HorizontalScrollView private val mediaContent: ViewGroup private val mediaPlayers: MutableMap = mutableMapOf() private val visualStabilityCallback = ::reorderAllPlayers + private var activeMediaIndex: Int = 0 + private var scrollIntoCurrentMedia: Int = 0 private var currentlyExpanded = true set(value) { @@ -49,29 +55,50 @@ class MediaViewManager @Inject constructor( } } } + private val scrollChangedListener = object : View.OnScrollChangeListener { + override fun onScrollChange(v: View?, scrollX: Int, scrollY: Int, oldScrollX: Int, + oldScrollY: Int) { + if (playerWidthPlusPadding == 0) { + return + } + onMediaScrollingChanged(scrollX / playerWidthPlusPadding, + scrollX % playerWidthPlusPadding) + } + } init { mediaCarousel = inflateMediaCarousel() + mediaCarousel.setOnScrollChangeListener(scrollChangedListener) mediaContent = mediaCarousel.requireViewById(R.id.media_carousel) mediaManager.addListener(object : MediaDataManager.Listener { override fun onMediaDataLoaded(key: String, data: MediaData) { updateView(key, data) + updatePlayerVisibilities() } override fun onMediaDataRemoved(key: String) { val removed = mediaPlayers.remove(key) removed?.apply { + val beforeActive = mediaContent.indexOfChild(removed.view) <= activeMediaIndex mediaContent.removeView(removed.view) removed.onDestroy() updateMediaPaddings() + if (beforeActive) { + // also update the index here since the scroll below might not always lead + // to a scrolling changed + activeMediaIndex = Math.max(0, activeMediaIndex - 1) + mediaCarousel.scrollX = Math.max(mediaCarousel.scrollX + - playerWidthPlusPadding, 0) + } + updatePlayerVisibilities() } } }) } - private fun inflateMediaCarousel(): ViewGroup { - return LayoutInflater.from(context).inflate( - R.layout.media_carousel, UniqueObjectHostView(context), false) as ViewGroup + private fun inflateMediaCarousel(): HorizontalScrollView { + return LayoutInflater.from(context).inflate(R.layout.media_carousel, + UniqueObjectHostView(context), false) as HorizontalScrollView } private fun reorderAllPlayers() { @@ -83,6 +110,26 @@ class MediaViewManager @Inject constructor( } } updateMediaPaddings() + updatePlayerVisibilities() + } + + private fun onMediaScrollingChanged(newIndex: Int, scrollInAmount: Int) { + val wasScrolledIn = scrollIntoCurrentMedia != 0 + scrollIntoCurrentMedia = scrollInAmount + val nowScrolledIn = scrollIntoCurrentMedia != 0 + if (newIndex != activeMediaIndex || wasScrolledIn != nowScrolledIn) { + activeMediaIndex = newIndex + updatePlayerVisibilities() + } + } + + private fun updatePlayerVisibilities() { + val scrolledIn = scrollIntoCurrentMedia != 0 + for (i in 0 until mediaContent.childCount) { + val view = mediaContent.getChildAt(i) + val visible = (i == activeMediaIndex) || ((i == (activeMediaIndex + 1)) && scrolledIn) + view.visibility = if (visible) View.VISIBLE else View.INVISIBLE + } } private fun updateView(key: String, data: MediaData) { @@ -127,8 +174,7 @@ class MediaViewManager @Inject constructor( private fun updatePlayerToCurrentState(existingPlayer: MediaControlPanel) { if (desiredState != null && desiredState!!.measurementInput != null) { // make sure the player width is set to the current state - val measurementInput = desiredState!!.measurementInput!! - existingPlayer.setPlayerWidth(measurementInput.width) + existingPlayer.setPlayerWidth(playerWidth) } } @@ -147,6 +193,10 @@ class MediaViewManager @Inject constructor( } + /** + * Set the current state of a view. This is updated often during animations and we shouldn't + * do anything expensive. + */ fun setCurrentState(state: MediaState) { currentState = state currentlyExpanded = state.expansion > 0 @@ -157,23 +207,58 @@ class MediaViewManager @Inject constructor( } /** - * @param targetState the target state we're transitioning to + * The desired location of this view has changed. We should remeasure the view to match + * the new bounds and kick off bounds animations if necessary. + * If an animation is happening, an animation is kicked of externally, which sets a new + * current state until we reach the targetState. + * + * @param desiredState the target state we're transitioning to * @param animate should this be animated */ - fun onDesiredStateChanged(targetState: MediaState?, animate: Boolean, duration: Long, - startDelay: Long) { - if (targetState is MediaHost.MediaHostState) { + fun onDesiredLocationChanged(desiredState: MediaState?, animate: Boolean, duration: Long, + startDelay: Long) { + if (desiredState is MediaHost.MediaHostState) { // This is a hosting view, let's remeasure our players - desiredState = targetState - val measurementInput = targetState.measurementInput - remeasureAllPlayers(measurementInput, animate, duration, startDelay) + this.desiredState = desiredState + val width = desiredState.boundsOnScreen.width() + if (playerWidth != width) { + setPlayerWidth(width) + for (mediaPlayer in mediaPlayers.values) { + if (animate && mediaPlayer.view.visibility == View.VISIBLE) { + mediaPlayer.animatePendingSizeChange(duration, startDelay) + } + } + val widthSpec = desiredState.measurementInput?.widthMeasureSpec ?: 0 + val heightSpec = desiredState.measurementInput?.heightMeasureSpec ?: 0 + var left = 0 + for (i in 0 until mediaContent.childCount) { + val view = mediaContent.getChildAt(i) + view.measure(widthSpec, heightSpec) + view.layout(left, 0, left + width, view.measuredHeight) + left = left + playerWidthPlusPadding + } + } } } - fun remeasureAllPlayers(measurementInput: MediaMeasurementInput?, - animate: Boolean, duration: Long, startDelay: Long) { - for (mediaPlayer in mediaPlayers.values) { - mediaPlayer.remeasure(measurementInput, animate, duration, startDelay) + fun setPlayerWidth(width: Int) { + if (width != playerWidth) { + playerWidth = width + playerWidthPlusPadding = playerWidth + context.resources.getDimensionPixelSize( + R.dimen.qs_media_padding) + for (mediaPlayer in mediaPlayers.values) { + mediaPlayer.setPlayerWidth(width) + } + // The player width has changed, let's update the scroll position to make sure + // it's still at the same place + var newScroll = activeMediaIndex * playerWidthPlusPadding + if (scrollIntoCurrentMedia > playerWidthPlusPadding) { + newScroll += playerWidthPlusPadding + - (scrollIntoCurrentMedia - playerWidthPlusPadding) + } else { + newScroll += scrollIntoCurrentMedia + } + mediaCarousel.scrollX = newScroll } } @@ -185,15 +270,33 @@ class MediaViewManager @Inject constructor( val firstPlayer = mediaPlayers.values.firstOrNull() ?: return null // Let's measure the size of the first player and return its height val previousProgress = firstPlayer.view.progress + val previousRight = firstPlayer.view.right + val previousBottom = firstPlayer.view.bottom firstPlayer.view.progress = input.expansion - firstPlayer.remeasure(input, false /* animate */, 0, 0) + firstPlayer.measure(input) + // Relayouting is necessary in motionlayout to obtain its size properly .... + firstPlayer.view.layout(0, 0, firstPlayer.view.measuredWidth, + firstPlayer.view.measuredHeight) val result = MeasurementOutput(firstPlayer.view.measuredWidth, firstPlayer.view.measuredHeight) firstPlayer.view.progress = previousProgress if (desiredState != null) { // remeasure it to the old size again! - firstPlayer.remeasure(desiredState!!.measurementInput, false, 0, 0) + firstPlayer.measure(desiredState!!.measurementInput) + firstPlayer.view.layout(0, 0, previousRight, previousBottom) } return result } + + fun onViewReattached() { + if (desiredState is MediaHost.MediaHostState) { + // HACK: MotionLayout doesn't always properly reevalate the state, let's kick of + // a measure to force it. + val widthSpec = desiredState!!.measurementInput?.widthMeasureSpec ?: 0 + val heightSpec = desiredState!!.measurementInput?.heightMeasureSpec ?: 0 + for (mediaPlayer in mediaPlayers.values) { + mediaPlayer.view.measure(widthSpec, heightSpec) + } + } + } } \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/media/UnboundHorizontalScrollView.kt b/packages/SystemUI/src/com/android/systemui/media/UnboundHorizontalScrollView.kt new file mode 100644 index 0000000000000..8efc9549068a4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/UnboundHorizontalScrollView.kt @@ -0,0 +1,31 @@ +package com.android.systemui.media + +import android.content.Context +import android.util.AttributeSet +import android.widget.HorizontalScrollView + +/** + * A Horizontal scrollview that doesn't limit itself to the childs bounds. This is useful + * when only measuring children but not the parent, when trying to apply a new scroll position + */ +class UnboundHorizontalScrollView @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) + : HorizontalScrollView(context, attrs, defStyleAttr) { + + /** + * Allow all scrolls to go through, use base implementation + */ + override fun scrollTo(x: Int, y: Int) { + if (mScrollX != x || mScrollY != y) { + val oldX: Int = mScrollX + val oldY: Int = mScrollY + mScrollX = x + mScrollY = y + invalidateParentCaches() + onScrollChanged(mScrollX, mScrollY, oldX, oldY) + if (!awakenScrollBars()) { + postInvalidateOnAnimation() + } + } + } +} \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/util/animation/UniqueObjectHostView.kt b/packages/SystemUI/src/com/android/systemui/util/animation/UniqueObjectHostView.kt index 376d09110d20e..bf94c5d36ff74 100644 --- a/packages/SystemUI/src/com/android/systemui/util/animation/UniqueObjectHostView.kt +++ b/packages/SystemUI/src/com/android/systemui/util/animation/UniqueObjectHostView.kt @@ -22,15 +22,20 @@ import android.view.View import android.widget.FrameLayout /** - * A special view that hosts a unique object, which only exists once, but can transition between - * different hosts. If the view currently hosts the unique object, it's measuring it normally, - * but if it's not attached, it will obtain the size by requesting a measure. + * A special view that is designed to host a single "unique object". The unique object is + * dynamically added and removed from this view and may transition to other UniqueObjectHostViews + * available in the system. + * This is useful to share a singular instance of a view that can transition between completely + * independent parts of the view hierarchy. + * If the view currently hosts the unique object, it's measuring it normally, + * but if it's not attached, it will obtain the size by requesting a measure, as if it were + * always attached. */ class UniqueObjectHostView( context: Context ) : FrameLayout(context) { lateinit var measurementCache : GuaranteedMeasurementCache - var firstMeasureListener: ((MeasurementInput) -> Unit)? = null + var onMeasureListener: ((MeasurementInput) -> Unit)? = null @SuppressLint("DrawAllocation") override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { @@ -41,13 +46,18 @@ class UniqueObjectHostView( val height = MeasureSpec.getSize(heightMeasureSpec) - paddingVertical val heightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.getMode(heightMeasureSpec)) val measurementInput = MeasurementInputData(widthSpec, heightSpec) - firstMeasureListener?.apply { + onMeasureListener?.apply { invoke(measurementInput) - firstMeasureListener = null } if (!isCurrentHost()) { // We're not currently the host, let's get the dimension from our cache (this might // perform a measuring if the cache doesn't have it yet) + // The goal here is that the view will always have a consistent measuring, regardless + // if it's attached or not. + // The behavior is therefore very similar to the view being persistently attached to + // this host, which can prevent flickers. It also makes sure that we always know + // the size of the view during transitions even if it has never been attached here + // before. val (cachedWidth, cachedHeight) = measurementCache.obtainMeasurement(measurementInput) setMeasuredDimension(cachedWidth + paddingHorizontal, cachedHeight + paddingVertical) } else {