diff options
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) } } |