summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/src/com/android/keyguard/KeyguardMedia.kt33
-rw-r--r--packages/SystemUI/src/com/android/keyguard/KeyguardMediaPlayer.java370
-rw-r--r--packages/SystemUI/tests/src/com/android/keyguard/KeyguardMediaPlayerTest.kt39
3 files changed, 298 insertions, 144 deletions
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardMedia.kt b/packages/SystemUI/src/com/android/keyguard/KeyguardMedia.kt
new file mode 100644
index 000000000000..487c29573a14
--- /dev/null
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardMedia.kt
@@ -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>
+)
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardMediaPlayer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardMediaPlayer.java
index 4fcacc276ac1..d1544346a25a 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardMediaPlayer.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardMediaPlayer.java
@@ -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);
-
- // 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));
+ 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;
+
+ 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();
- // 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);
- }
+ // App Icon
+ Drawable appIconDrawable = appIcon.loadDrawable(mContext);
- // 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);
- }
+ // Song name
+ String song = mediaMetadata.getString(MediaMetadata.METADATA_KEY_TITLE);
- // Background color
- if (mMediaNotifView instanceof MediaHeaderView) {
- MediaHeaderView head = (MediaHeaderView) mMediaNotifView;
- head.setBackgroundColor(bgColor);
- }
+ // Artist name
+ String artist = mediaMetadata.getString(MediaMetadata.METADATA_KEY_ARTIST);
- // 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);
+ // 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);
- 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);
+ 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);
}
- 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;
+ /** 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);
+ }
}
- 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);
- }
- });
+ 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);
+ }
}
- /**
- * 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");
+ /** 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]));
+ }
}
- // 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);
- });
- }
+ /** 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);
- private void loadDimens() {
- mAlbumArtRadius = mContext.getResources().getDimension(R.dimen.qs_media_corner_radius);
- mAlbumArtSize = (int) mContext.getResources().getDimension(
- R.dimen.qs_media_album_size);
+ // Background color
+ if (mMediaHeaderView != null) {
+ mMediaHeaderView.setBackgroundColor(data.getBackgroundColor());
+ }
+
+ // Album art
+ if (mAlbumView != null) {
+ mAlbumView.setImageDrawable(data.getArtwork());
+ mAlbumView.setVisibility(data.getArtwork() == null ? View.GONE : View.VISIBLE);
+ }
+
+ // App icon
+ if (mAppIconView != null) {
+ Drawable iconDrawable = data.getAppIcon();
+ iconDrawable.setTint(data.getForegroundColor());
+ mAppIconView.setImageDrawable(iconDrawable);
+ }
+
+ // App name
+ if (mAppNameView != null) {
+ String appNameString = data.getApp();
+ mAppNameView.setText(appNameString);
+ mAppNameView.setTextColor(data.getForegroundColor());
+ }
+
+ // Song name
+ if (mTitleView != null) {
+ mTitleView.setText(data.getSong());
+ mTitleView.setTextColor(data.getForegroundColor());
+ }
+
+ // Artist name
+ if (mArtistView != null) {
+ mArtistView.setText(data.getArtist());
+ mArtistView.setTextColor(data.getForegroundColor());
+ }
+
+ // Control buttons
+ for (int i = 0; i < ACTION_IDS.length; i++) {
+ ImageButton button = mButtonViews.get(i);
+ if (button == null) {
+ continue;
+ }
+ Drawable icon = data.getActionIcons().get(i);
+ if (icon == null) {
+ button.setVisibility(View.GONE);
+ button.setImageDrawable(null);
+ } else {
+ button.setVisibility(View.VISIBLE);
+ button.setImageDrawable(icon);
+ button.setImageTintList(ColorStateList.valueOf(data.getForegroundColor()));
+ }
+ }
+ }
}
}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMediaPlayerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMediaPlayerTest.kt
index 464a740c931c..072bc446fd21 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMediaPlayerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMediaPlayerTest.kt
@@ -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)
}
}