Merge "Separate updating views from deriving state" into rvc-dev am: e174ee831e

Change-Id: I030f4c7ec5e4e3c92f03e6b7129fee84c6538201
This commit is contained in:
TreeHugger Robot
2020-03-25 19:14:03 +00:00
committed by Automerger Merge Worker
3 changed files with 303 additions and 149 deletions

View File

@@ -0,0 +1,33 @@
/*
* 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.graphics.drawable.Drawable
import java.util.List
/** State for lock screen media controls. */
data class KeyguardMedia(
val foregroundColor: Int,
val backgroundColor: Int,
val app: String?,
val appIcon: Drawable?,
val artist: String?,
val song: String?,
val artwork: Drawable?,
val actionIcons: List<Drawable>
)

View File

@@ -32,6 +32,9 @@ 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;
@@ -64,39 +67,47 @@ public class KeyguardMediaPlayer {
private final Context mContext;
private final Executor mBackgroundExecutor;
private float mAlbumArtRadius;
private int mAlbumArtSize;
private View mMediaNotifView;
private final KeyguardMediaViewModel mViewModel;
private KeyguardMediaObserver mObserver;
@Inject
public KeyguardMediaPlayer(Context context, @Background Executor backgroundExecutor) {
mContext = context;
mBackgroundExecutor = backgroundExecutor;
loadDimens();
mViewModel = new KeyguardMediaViewModel(context);
}
/** Binds media controls to a view hierarchy. */
public void bindView(View v) {
if (mMediaNotifView != null) {
if (mObserver != null) {
throw new IllegalStateException("cannot bind views, already bound");
}
mMediaNotifView = v;
loadDimens();
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 (mMediaNotifView == null) {
if (mObserver == null) {
throw new IllegalStateException("cannot unbind views, nothing bound");
}
mMediaNotifView = null;
mViewModel.getKeyguardMedia().removeObserver(mObserver);
mObserver = null;
}
/** Clear the media controls because there isn't an active session. */
public void clearControls() {
if (mMediaNotifView != null) {
mMediaNotifView.setVisibility(View.GONE);
}
mBackgroundExecutor.execute(mViewModel::clearControls);
}
/**
@@ -110,159 +121,244 @@ public class KeyguardMediaPlayer {
*/
public void updateControls(NotificationEntry entry, Icon appIcon,
MediaMetadata mediaMetadata) {
if (mMediaNotifView == null) {
if (mObserver == null) {
throw new IllegalStateException("cannot update controls, views not bound");
}
if (mediaMetadata == null) {
mMediaNotifView.setVisibility(View.GONE);
Log.d(TAG, "media metadata was 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;
}
mMediaNotifView.setVisibility(View.VISIBLE);
mBackgroundExecutor.execute(() -> mViewModel.updateControls(entry, appIcon, mediaMetadata));
}
Notification notif = entry.getSbn().getNotification();
/** ViewModel for KeyguardMediaControls. */
private static final class KeyguardMediaViewModel {
// Computed foreground and background color based on album art.
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);
private final Context mContext;
private final MutableLiveData<KeyguardMedia> mMedia = new MutableLiveData<>();
private final Object mActionsLock = new Object();
private List<PendingIntent> mActions;
private float mAlbumArtRadius;
private int mAlbumArtSize;
// Album art
ImageView albumView = mMediaNotifView.findViewById(R.id.album_art);
if (albumView != null) {
// Resize art in a background thread
final Bitmap bm = artworkBitmap;
mBackgroundExecutor.execute(() -> processAlbumArt(bm, albumView));
KeyguardMediaViewModel(Context context) {
mContext = context;
loadDimens();
}
// App icon
ImageView appIconView = mMediaNotifView.findViewById(R.id.icon);
if (appIconView != null) {
Drawable iconDrawable = appIcon.loadDrawable(mContext);
iconDrawable.setTint(fgColor);
appIconView.setImageDrawable(iconDrawable);
/** Close the media player because there isn't an active session. */
public void clearControls() {
synchronized (mActionsLock) {
mActions = null;
}
mMedia.postValue(null);
}
// App name
TextView appName = mMediaNotifView.findViewById(R.id.app_name);
if (appName != null) {
/** Update the media player with information about the active session. */
public void updateControls(NotificationEntry entry, Icon appIcon,
MediaMetadata mediaMetadata) {
// 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 appNameString = builder.loadHeaderAppName();
appName.setText(appNameString);
appName.setTextColor(fgColor);
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);
}
// Song name
TextView titleText = mMediaNotifView.findViewById(R.id.header_title);
if (titleText != null) {
String songName = mediaMetadata.getString(MediaMetadata.METADATA_KEY_TITLE);
titleText.setText(songName);
titleText.setTextColor(fgColor);
/** Gets state for the lock screen media controls. */
public LiveData<KeyguardMedia> getKeyguardMedia() {
return mMedia;
}
// Artist name
TextView artistText = mMediaNotifView.findViewById(R.id.header_artist);
if (artistText != null) {
String artistName = mediaMetadata.getString(MediaMetadata.METADATA_KEY_ARTIST);
artistText.setText(artistName);
artistText.setTextColor(fgColor);
}
// Background color
if (mMediaNotifView instanceof MediaHeaderView) {
MediaHeaderView head = (MediaHeaderView) mMediaNotifView;
head.setBackgroundColor(bgColor);
}
// Control buttons
final List<Icon> icons = new ArrayList<>();
final List<PendingIntent> intents = new ArrayList<>();
Notification.Action[] actions = notif.actions;
final int[] actionsToShow = notif.extras.getIntArray(Notification.EXTRA_COMPACT_ACTIONS);
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];
icons.add(actions[idx].getIcon());
intents.add(actions[idx].actionIntent);
} else {
icons.add(null);
intents.add(null);
/**
* 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);
}
}
}
Context packageContext = entry.getSbn().getPackageContext(mContext);
for (int i = 0; i < ACTION_IDS.length; i++) {
ImageButton button = mMediaNotifView.findViewById(ACTION_IDS[i]);
if (button == null) {
continue;
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]));
}
Icon icon = icons.get(i);
if (icon == null) {
button.setVisibility(View.GONE);
} else {
button.setVisibility(View.VISIBLE);
button.setImageDrawable(icon.loadDrawable(packageContext));
button.setImageTintList(ColorStateList.valueOf(fgColor));
final PendingIntent intent = intents.get(i);
if (intent != null) {
button.setOnClickListener(v -> {
try {
intent.send();
} catch (PendingIntent.CanceledException e) {
Log.d(TAG, "failed to send action intent", e);
}
});
}
/** 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()));
}
}
}
}
/**
* Process album art for layout
* @param albumArt bitmap to use for album art
* @param albumView view to hold the album art
*/
private void processAlbumArt(Bitmap albumArt, ImageView albumView) {
RoundedBitmapDrawable roundedDrawable = null;
if (albumArt != null) {
Bitmap original = albumArt.copy(Bitmap.Config.ARGB_8888, true);
Bitmap scaled = Bitmap.createScaledBitmap(original, mAlbumArtSize, mAlbumArtSize,
false);
roundedDrawable = RoundedBitmapDrawableFactory.create(mContext.getResources(), scaled);
roundedDrawable.setCornerRadius(mAlbumArtRadius);
} else {
Log.e(TAG, "No album art available");
}
// Now that it's resized, update the UI
final RoundedBitmapDrawable result = roundedDrawable;
albumView.post(() -> {
albumView.setImageDrawable(result);
albumView.setVisibility(result == null ? View.GONE : View.VISIBLE);
});
}
private void loadDimens() {
mAlbumArtRadius = mContext.getResources().getDimension(R.dimen.qs_media_corner_radius);
mAlbumArtSize = (int) mContext.getResources().getDimension(
R.dimen.qs_media_album_size);
}
}

View File

@@ -22,6 +22,8 @@ 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
@@ -50,25 +52,46 @@ public class KeyguardMediaPlayerTest : SysuiTestCase() {
private lateinit var mediaMetadata: MediaMetadata.Builder
private lateinit var entry: NotificationEntryBuilder
@Mock private lateinit var mockView: View
private lateinit var textView: TextView
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() {
fakeExecutor = FakeExecutor(FakeSystemClock())
keyguardMediaPlayer = KeyguardMediaPlayer(context, fakeExecutor)
mockView = mock(View::class.java)
textView = TextView(context)
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()
ArchTaskExecutor.getInstance().setDelegate(taskExecutor)
keyguardMediaPlayer.bindView(mockView)
}
@After
public fun tearDown() {
keyguardMediaPlayer.unbindView()
ArchTaskExecutor.getInstance().setDelegate(null)
}
@Test
@@ -87,34 +110,36 @@ public class KeyguardMediaPlayerTest : SysuiTestCase() {
@Test
public fun testUpdateControls() {
keyguardMediaPlayer.updateControls(entry.build(), 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 testSongName() {
whenever<TextView>(mockView.findViewById(R.id.header_title)).thenReturn(textView)
val song: String = "Song"
mediaMetadata.putText(MediaMetadata.METADATA_KEY_TITLE, song)
keyguardMediaPlayer.updateControls(entry.build(), mockIcon, mediaMetadata.build())
assertThat(textView.getText()).isEqualTo(song)
assertThat(fakeExecutor.runAllReady()).isEqualTo(1)
assertThat(songView.getText()).isEqualTo(song)
}
@Test
public fun testArtistName() {
whenever<TextView>(mockView.findViewById(R.id.header_artist)).thenReturn(textView)
val artist: String = "Artist"
mediaMetadata.putText(MediaMetadata.METADATA_KEY_ARTIST, artist)
keyguardMediaPlayer.updateControls(entry.build(), mockIcon, mediaMetadata.build())
assertThat(textView.getText()).isEqualTo(artist)
assertThat(fakeExecutor.runAllReady()).isEqualTo(1)
assertThat(artistView.getText()).isEqualTo(artist)
}
}