From 5dbef2d99c4c16b33a8331bd656e4373c7ec85bf Mon Sep 17 00:00:00 2001 From: Selim Cinek Date: Thu, 7 May 2020 17:44:38 -0700 Subject: [PATCH] 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);