blob: af5196f92bcbaa0596d6e1a96bac6ed92f13b68b [file] [log] [blame]
/*
* 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()));
}
}
}
}
}