Loading packages/SystemUI/src/com/android/keyguard/KeyguardMedia.kt 0 → 100644 +33 −0 Original line number Diff line number Diff line /* * 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> ) packages/SystemUI/src/com/android/keyguard/KeyguardMediaPlayer.java +233 −137 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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); } /** Loading @@ -110,19 +121,49 @@ 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 { 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(); } /** 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) { // Computed foreground and background color based on album art. // 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); Loading @@ -144,125 +185,180 @@ public class KeyguardMediaPlayer { 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)); } // App icon ImageView appIconView = mMediaNotifView.findViewById(R.id.icon); if (appIconView != null) { Drawable iconDrawable = appIcon.loadDrawable(mContext); iconDrawable.setTint(fgColor); appIconView.setImageDrawable(iconDrawable); 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 TextView appName = mMediaNotifView.findViewById(R.id.app_name); if (appName != null) { 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 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); } String song = mediaMetadata.getString(MediaMetadata.METADATA_KEY_TITLE); // 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); } String artist = mediaMetadata.getString(MediaMetadata.METADATA_KEY_ARTIST); // Control buttons final List<Icon> icons = new ArrayList<>(); 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); 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]; icons.add(actions[idx].getIcon()); actionIcons.add(actions[idx].getIcon().loadDrawable(packageContext)); intents.add(actions[idx].actionIntent); } else { icons.add(null); actionIcons.add(null); intents.add(null); } } synchronized (mActionsLock) { mActions = intents; } 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; 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); } } 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); } }); } } 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())); } } } } } packages/SystemUI/tests/src/com/android/keyguard/KeyguardMediaPlayerTest.kt +32 −7 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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 Loading @@ -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) } } Loading
packages/SystemUI/src/com/android/keyguard/KeyguardMedia.kt 0 → 100644 +33 −0 Original line number Diff line number Diff line /* * 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> )
packages/SystemUI/src/com/android/keyguard/KeyguardMediaPlayer.java +233 −137 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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); } /** Loading @@ -110,19 +121,49 @@ 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 { 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(); } /** 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) { // Computed foreground and background color based on album art. // 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); Loading @@ -144,125 +185,180 @@ public class KeyguardMediaPlayer { 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)); } // App icon ImageView appIconView = mMediaNotifView.findViewById(R.id.icon); if (appIconView != null) { Drawable iconDrawable = appIcon.loadDrawable(mContext); iconDrawable.setTint(fgColor); appIconView.setImageDrawable(iconDrawable); 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 TextView appName = mMediaNotifView.findViewById(R.id.app_name); if (appName != null) { 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 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); } String song = mediaMetadata.getString(MediaMetadata.METADATA_KEY_TITLE); // 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); } String artist = mediaMetadata.getString(MediaMetadata.METADATA_KEY_ARTIST); // Control buttons final List<Icon> icons = new ArrayList<>(); 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); 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]; icons.add(actions[idx].getIcon()); actionIcons.add(actions[idx].getIcon().loadDrawable(packageContext)); intents.add(actions[idx].actionIntent); } else { icons.add(null); actionIcons.add(null); intents.add(null); } } synchronized (mActionsLock) { mActions = intents; } 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; 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); } } 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); } }); } } 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())); } } } } }
packages/SystemUI/tests/src/com/android/keyguard/KeyguardMediaPlayerTest.kt +32 −7 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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 Loading @@ -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) } }