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
This commit is contained in:
Selim Cinek
2020-05-07 17:44:38 -07:00
parent d835792bb0
commit 5dbef2d99c
29 changed files with 892 additions and 1438 deletions

View File

@@ -45,109 +45,4 @@
android:layout_height="match_parent"
/>
<!-- Layout for media controls. -->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/keyguard_media_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center"
android:padding="16dp"
>
<ImageView
android:id="@+id/album_art"
android:layout_width="@dimen/qs_media_album_size"
android:layout_height="@dimen/qs_media_album_size"
android:layout_marginRight="16dp"
android:layout_weight="0"
/>
<!-- Media information -->
<LinearLayout
android:orientation="vertical"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
>
<LinearLayout
android:orientation="horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
>
<com.android.internal.widget.CachingIconView
android:id="@+id/icon"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginEnd="5dp"
/>
<TextView
android:id="@+id/app_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp"
android:singleLine="true"
/>
</LinearLayout>
<!-- Song name -->
<TextView
android:id="@+id/header_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="true"
android:fontFamily="@*android:string/config_headlineFontFamilyMedium"
android:textSize="18sp"
android:paddingBottom="6dp"
android:gravity="center"/>
<!-- Artist name -->
<TextView
android:id="@+id/header_artist"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@*android:string/config_bodyFontFamily"
android:textSize="14sp"
android:singleLine="true"
/>
</LinearLayout>
<!-- Controls -->
<LinearLayout
android:id="@+id/media_actions"
android:orientation="horizontal"
android:layoutDirection="ltr"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center"
android:layout_gravity="center"
>
<ImageButton
style="@style/MediaPlayer.Button"
android:layout_width="48dp"
android:layout_height="48dp"
android:gravity="center"
android:visibility="gone"
android:id="@+id/action0"
/>
<ImageButton
style="@style/MediaPlayer.Button"
android:layout_width="48dp"
android:layout_height="48dp"
android:gravity="center"
android:visibility="gone"
android:id="@+id/action1"
/>
<ImageButton
style="@style/MediaPlayer.Button"
android:layout_width="48dp"
android:layout_height="48dp"
android:gravity="center"
android:visibility="gone"
android:id="@+id/action2"
/>
</LinearLayout>
</LinearLayout>
</com.android.systemui.statusbar.notification.stack.MediaHeaderView>

View File

@@ -22,11 +22,10 @@
android:layout_height="wrap_content"
android:padding="@dimen/qs_media_padding"
android:scrollbars="none"
android:visibility="gone"
>
<LinearLayout
android:id="@+id/media_carousel"
android:layout_width="match_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
>

View File

@@ -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<KeyguardMedia> mMedia = new MutableLiveData<>();
private final Object mActionsLock = new Object();
private List<PendingIntent> 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<Drawable> actionIcons = new ArrayList<>();
final List<PendingIntent> 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<KeyguardMedia> 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<KeyguardMedia> {
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<ImageButton> 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()));
}
}
}
}
}

View File

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

View File

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

View File

@@ -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<ResolveInfo> 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<Integer> actionsWhenCollapsed = data.getActionsToShowInCompact();
// Media controls
int i = 0;
List<MediaAction> actionIcons = data.getActions();
for (; i < actionIcons.size() && i < ACTION_IDS.length; i++) {
final ImageButton button = mMediaNotifView.findViewById(ACTION_IDS[i]);
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

View File

@@ -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<Drawable>
val actions: List<MediaAction>,
val actionsToShowInCompact: List<Int>,
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?
)

View File

@@ -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<String, MediaData> = 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<MediaAction> = 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)

View File

@@ -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<String, MediaControlPanel> = mutableMapOf()
private val mediaHosts = arrayOfNulls<ViewGroup>(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
}
}

View File

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

View File

@@ -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();
}
}

View File

@@ -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<TileRecord> 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<QSMediaPlayer> 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() {

View File

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

View File

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

View File

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

View File

@@ -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<NotificationShadeWindowController> 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<MediaListener> 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<MediaListener> 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<MediaListener> 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) {}
}
}

View File

@@ -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);
}
/** */

View File

@@ -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;
}

View File

@@ -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) {
}
/**

View File

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

View File

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

View File

@@ -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;
}
}

View File

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

View File

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

View File

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

View File

@@ -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<TextView>(mockView.findViewById(R.id.header_title)).thenReturn(songView)
whenever<TextView>(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)
}
}

View File

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

View File

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

View File

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