Loading packages/SystemUI/res/layout/keyguard_media_header.xml 0 → 100644 +153 −0 Original line number Diff line number Diff line <?xml version="1.0" encoding="utf-8"?> <!-- ~ 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 --> <!-- Layout for media controls on the lockscreen --> <com.android.systemui.statusbar.notification.stack.MediaHeaderView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingStart="0dp" android:paddingEnd="0dp" android:focusable="true" android:clickable="true" > <!-- Background views required by ActivatableNotificationView. --> <com.android.systemui.statusbar.notification.row.NotificationBackgroundView android:id="@+id/backgroundNormal" android:layout_width="match_parent" android:layout_height="match_parent" /> <com.android.systemui.statusbar.notification.row.NotificationBackgroundView android:id="@+id/backgroundDimmed" android:layout_width="match_parent" android:layout_height="match_parent" /> <com.android.systemui.statusbar.notification.FakeShadowView android:id="@+id/fake_shadow" android:layout_width="match_parent" android:layout_height="match_parent" /> <!-- Layout for media controls. --> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/keyguard_media_view" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:gravity="center_horizontal|fill_vertical" android:padding="16dp" > <ImageView android:id="@+id/album_art" android:layout_width="@dimen/qs_media_album_size" android:layout_height="@dimen/qs_media_album_size" android:layout_marginRight="16dp" android:layout_weight="0" /> <!-- Media information --> <LinearLayout android:orientation="vertical" android:layout_width="0dp" android:layout_height="@dimen/qs_media_album_size" android:layout_weight="1" > <LinearLayout android:orientation="horizontal" android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="center" > <com.android.internal.widget.CachingIconView android:id="@+id/icon" android:layout_width="16dp" android:layout_height="16dp" android:layout_marginEnd="5dp" /> <TextView android:id="@+id/app_name" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="14sp" android:singleLine="true" /> </LinearLayout> <!-- Song name --> <TextView android:id="@+id/header_title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:singleLine="true" android:fontFamily="@*android:string/config_headlineFontFamilyMedium" android:textSize="18sp" android:paddingBottom="6dp" android:gravity="center"/> <!-- Artist name --> <TextView android:id="@+id/header_artist" android:layout_width="wrap_content" android:layout_height="wrap_content" android:fontFamily="@*android:string/config_bodyFontFamily" android:textSize="14sp" android:singleLine="true" /> </LinearLayout> <!-- Controls --> <LinearLayout android:id="@+id/media_actions" android:orientation="horizontal" android:layoutDirection="ltr" android:layout_width="wrap_content" android:layout_height="match_parent" android:gravity="center" android:layout_gravity="center" > <ImageButton style="@android:style/Widget.Material.Button.Borderless.Small" android:layout_width="48dp" android:layout_height="48dp" android:gravity="center" android:visibility="gone" android:id="@+id/action0" /> <ImageButton style="@android:style/Widget.Material.Button.Borderless.Small" android:layout_width="48dp" android:layout_height="48dp" android:gravity="center" android:visibility="gone" android:id="@+id/action1" /> <ImageButton style="@android:style/Widget.Material.Button.Borderless.Small" android:layout_width="48dp" android:layout_height="48dp" android:gravity="center" android:visibility="gone" android:id="@+id/action2" /> </LinearLayout> </LinearLayout> </com.android.systemui.statusbar.notification.stack.MediaHeaderView> packages/SystemUI/src/com/android/keyguard/KeyguardMediaPlayer.java 0 → 100644 +266 −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.app.Notification; import android.app.PendingIntent; import android.content.Context; import android.content.res.ColorStateList; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; import android.media.MediaMetadata; import android.util.Log; import android.view.View; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.TextView; import androidx.core.graphics.drawable.RoundedBitmapDrawable; import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory; import androidx.palette.graphics.Palette; import com.android.internal.util.ContrastColorUtil; import com.android.systemui.R; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.statusbar.notification.MediaNotificationProcessor; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.stack.MediaHeaderView; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Singleton; /** * Media controls to display on the lockscreen * * TODO: Should extend MediaControlPanel to avoid code duplication. * Unfortunately, it isn't currently possible because the ActivatableNotificationView background is * different. */ @Singleton public class KeyguardMediaPlayer { private static final String TAG = "KeyguardMediaPlayer"; // Buttons that can be displayed on lock screen media controls. private static final int[] ACTION_IDS = {R.id.action0, R.id.action1, R.id.action2}; private final Context mContext; private final Executor mBackgroundExecutor; private float mAlbumArtRadius; private int mAlbumArtSize; private View mMediaNotifView; @Inject public KeyguardMediaPlayer(Context context, @Background Executor backgroundExecutor) { mContext = context; mBackgroundExecutor = backgroundExecutor; loadDimens(); } /** Binds media controls to a view hierarchy. */ public void bindView(View v) { if (mMediaNotifView != null) { throw new IllegalStateException("cannot bind views, already bound"); } mMediaNotifView = v; loadDimens(); } /** Unbinds media controls. */ public void unbindView() { if (mMediaNotifView == null) { throw new IllegalStateException("cannot unbind views, nothing bound"); } mMediaNotifView = null; } /** Clear the media controls because there isn't an active session. */ public void clearControls() { if (mMediaNotifView != null) { mMediaNotifView.setVisibility(View.GONE); } } /** * Update the media player * * TODO: consider registering a MediaLister instead of exposing this update method. * * @param entry Media notification that will be used to update the player * @param appIcon Icon for the app playing the media * @param mediaMetadata Media metadata that will be used to update the player */ public void updateControls(NotificationEntry entry, Icon appIcon, MediaMetadata mediaMetadata) { if (mMediaNotifView == null) { throw new IllegalStateException("cannot update controls, views not bound"); } if (mediaMetadata == null) { throw new IllegalArgumentException("media metadata was null"); } mMediaNotifView.setVisibility(View.VISIBLE); Notification notif = entry.getSbn().getNotification(); // 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)); } // App icon ImageView appIconView = mMediaNotifView.findViewById(R.id.icon); if (appIconView != null) { Drawable iconDrawable = appIcon.loadDrawable(mContext); iconDrawable.setTint(fgColor); appIconView.setImageDrawable(iconDrawable); } // 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); } // 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); } // 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); } } 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; } 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); } }); } } } } /** * 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); } } packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java +17 −1 Original line number Diff line number Diff line Loading @@ -46,6 +46,7 @@ import android.widget.ImageView; import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; import com.android.internal.statusbar.NotificationVisibility; import com.android.keyguard.KeyguardMediaPlayer; import com.android.systemui.Dependency; import com.android.systemui.Dumpable; import com.android.systemui.Interpolators; Loading @@ -65,6 +66,7 @@ import com.android.systemui.statusbar.phone.ScrimState; import com.android.systemui.statusbar.phone.StatusBar; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.util.DeviceConfigProxy; import com.android.systemui.util.Utils; import java.io.FileDescriptor; import java.io.PrintWriter; Loading Loading @@ -111,6 +113,7 @@ public class NotificationMediaManager implements Dumpable { private ScrimController mScrimController; @Nullable private LockscreenWallpaper mLockscreenWallpaper; private final KeyguardMediaPlayer mMediaPlayer; private final Executor mMainExecutor; Loading Loading @@ -184,11 +187,13 @@ public class NotificationMediaManager implements Dumpable { NotificationEntryManager notificationEntryManager, MediaArtworkProcessor mediaArtworkProcessor, KeyguardBypassController keyguardBypassController, KeyguardMediaPlayer keyguardMediaPlayer, @Main Executor mainExecutor, DeviceConfigProxy deviceConfig) { mContext = context; mMediaArtworkProcessor = mediaArtworkProcessor; mKeyguardBypassController = keyguardBypassController; mMediaPlayer = keyguardMediaPlayer; mMediaListeners = new ArrayList<>(); // TODO: use MediaSessionManager.SessionListener to hook us up to future updates // in session state Loading Loading @@ -468,6 +473,7 @@ public class NotificationMediaManager implements Dumpable { && mBiometricUnlockController.isWakeAndUnlock(); if (mKeyguardStateController.isLaunchTransitionFadingAway() || wakeAndUnlock) { mBackdrop.setVisibility(View.INVISIBLE); mMediaPlayer.clearControls(); Trace.endSection(); return; } Loading @@ -490,6 +496,14 @@ public class NotificationMediaManager implements Dumpable { } } NotificationEntry entry = mEntryManager .getActiveNotificationUnfiltered(mMediaNotificationKey); if (entry != null) { mMediaPlayer.updateControls(entry, getMediaIcon(), mediaMetadata); } else { mMediaPlayer.clearControls(); } // Process artwork on a background thread and send the resulting bitmap to // finishUpdateMediaMetaData. if (metaDataChanged) { Loading @@ -498,7 +512,7 @@ public class NotificationMediaManager implements Dumpable { } mProcessArtworkTasks.clear(); } if (artworkBitmap != null) { if (artworkBitmap != null && !Utils.useQsMediaPlayer(mContext)) { mProcessArtworkTasks.add(new ProcessArtworkTask(this, metaDataChanged, allowEnterAnimation).execute(artworkBitmap)); } else { Loading Loading @@ -612,6 +626,7 @@ public class NotificationMediaManager implements Dumpable { // We are unlocking directly - no animation! mBackdrop.setVisibility(View.GONE); mBackdropBack.setImageDrawable(null); mMediaPlayer.clearControls(); if (windowController != null) { windowController.setBackdropShowing(false); } Loading @@ -628,6 +643,7 @@ public class NotificationMediaManager implements Dumpable { mBackdrop.setVisibility(View.GONE); mBackdropFront.animate().cancel(); mBackdropBack.setImageDrawable(null); mMediaPlayer.clearControls(); mMainExecutor.execute(mHideBackdropFront); }); if (mKeyguardStateController.isKeyguardFadingAway()) { Loading packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java +3 −0 Original line number Diff line number Diff line Loading @@ -21,6 +21,7 @@ import android.content.Context; import android.os.Handler; import com.android.internal.statusbar.IStatusBarService; import com.android.keyguard.KeyguardMediaPlayer; import com.android.systemui.bubbles.BubbleController; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.plugins.statusbar.StatusBarStateController; Loading Loading @@ -93,6 +94,7 @@ public interface StatusBarDependenciesModule { NotificationEntryManager notificationEntryManager, MediaArtworkProcessor mediaArtworkProcessor, KeyguardBypassController keyguardBypassController, KeyguardMediaPlayer keyguardMediaPlayer, @Main Executor mainExecutor, DeviceConfigProxy deviceConfigProxy) { return new NotificationMediaManager( Loading @@ -102,6 +104,7 @@ public interface StatusBarDependenciesModule { notificationEntryManager, mediaArtworkProcessor, keyguardBypassController, keyguardMediaPlayer, mainExecutor, deviceConfigProxy); } Loading packages/SystemUI/src/com/android/systemui/statusbar/notification/MediaNotificationProcessor.java +14 −7 Original line number Diff line number Diff line Loading @@ -152,7 +152,13 @@ public class MediaNotificationProcessor { } } private int selectForegroundColor(int backgroundColor, Palette palette) { /** * Select a foreground color depending on whether the background color is dark or light * @param backgroundColor Background color to coordinate with * @param palette Artwork palette, should be obtained from {@link generateArtworkPaletteBuilder} * @return foreground color */ public static int selectForegroundColor(int backgroundColor, Palette palette) { if (ContrastColorUtil.isColorLight(backgroundColor)) { return selectForegroundColorForSwatches(palette.getDarkVibrantSwatch(), palette.getVibrantSwatch(), Loading @@ -170,7 +176,7 @@ public class MediaNotificationProcessor { } } private int selectForegroundColorForSwatches(Palette.Swatch moreVibrant, private static int selectForegroundColorForSwatches(Palette.Swatch moreVibrant, Palette.Swatch vibrant, Palette.Swatch moreMutedSwatch, Palette.Swatch mutedSwatch, Palette.Swatch dominantSwatch, int fallbackColor) { Palette.Swatch coloredCandidate = selectVibrantCandidate(moreVibrant, vibrant); Loading @@ -194,7 +200,7 @@ public class MediaNotificationProcessor { } } private Palette.Swatch selectMutedCandidate(Palette.Swatch first, private static Palette.Swatch selectMutedCandidate(Palette.Swatch first, Palette.Swatch second) { boolean firstValid = hasEnoughPopulation(first); boolean secondValid = hasEnoughPopulation(second); Loading @@ -215,7 +221,8 @@ public class MediaNotificationProcessor { return null; } private Palette.Swatch selectVibrantCandidate(Palette.Swatch first, Palette.Swatch second) { private static Palette.Swatch selectVibrantCandidate(Palette.Swatch first, Palette.Swatch second) { boolean firstValid = hasEnoughPopulation(first); boolean secondValid = hasEnoughPopulation(second); if (firstValid && secondValid) { Loading @@ -235,7 +242,7 @@ public class MediaNotificationProcessor { return null; } private boolean hasEnoughPopulation(Palette.Swatch swatch) { private static boolean hasEnoughPopulation(Palette.Swatch swatch) { // We want a fraction that is at least 1% of the image return swatch != null && (swatch.getPopulation() / (float) RESIZE_BITMAP_AREA > MINIMUM_IMAGE_FRACTION); Loading @@ -257,7 +264,7 @@ public class MediaNotificationProcessor { * @param palette Artwork palette, should be obtained from {@link generateArtworkPaletteBuilder} * @return Swatch that should be used as the background of the media notification. */ private static Palette.Swatch findBackgroundSwatch(Palette palette) { public static Palette.Swatch findBackgroundSwatch(Palette palette) { // by default we use the dominant palette Palette.Swatch dominantSwatch = palette.getDominantSwatch(); if (dominantSwatch == null) { Loading Loading @@ -301,7 +308,7 @@ public class MediaNotificationProcessor { * @param artwork Media artwork * @return Builder that generates the {@link Palette} for the media artwork. */ private static Palette.Builder generateArtworkPaletteBuilder(Bitmap artwork) { public static Palette.Builder generateArtworkPaletteBuilder(Bitmap artwork) { // for the background we only take the left side of the image to ensure // a smooth transition return Palette.from(artwork) Loading Loading
packages/SystemUI/res/layout/keyguard_media_header.xml 0 → 100644 +153 −0 Original line number Diff line number Diff line <?xml version="1.0" encoding="utf-8"?> <!-- ~ 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 --> <!-- Layout for media controls on the lockscreen --> <com.android.systemui.statusbar.notification.stack.MediaHeaderView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingStart="0dp" android:paddingEnd="0dp" android:focusable="true" android:clickable="true" > <!-- Background views required by ActivatableNotificationView. --> <com.android.systemui.statusbar.notification.row.NotificationBackgroundView android:id="@+id/backgroundNormal" android:layout_width="match_parent" android:layout_height="match_parent" /> <com.android.systemui.statusbar.notification.row.NotificationBackgroundView android:id="@+id/backgroundDimmed" android:layout_width="match_parent" android:layout_height="match_parent" /> <com.android.systemui.statusbar.notification.FakeShadowView android:id="@+id/fake_shadow" android:layout_width="match_parent" android:layout_height="match_parent" /> <!-- Layout for media controls. --> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/keyguard_media_view" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:gravity="center_horizontal|fill_vertical" android:padding="16dp" > <ImageView android:id="@+id/album_art" android:layout_width="@dimen/qs_media_album_size" android:layout_height="@dimen/qs_media_album_size" android:layout_marginRight="16dp" android:layout_weight="0" /> <!-- Media information --> <LinearLayout android:orientation="vertical" android:layout_width="0dp" android:layout_height="@dimen/qs_media_album_size" android:layout_weight="1" > <LinearLayout android:orientation="horizontal" android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="center" > <com.android.internal.widget.CachingIconView android:id="@+id/icon" android:layout_width="16dp" android:layout_height="16dp" android:layout_marginEnd="5dp" /> <TextView android:id="@+id/app_name" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="14sp" android:singleLine="true" /> </LinearLayout> <!-- Song name --> <TextView android:id="@+id/header_title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:singleLine="true" android:fontFamily="@*android:string/config_headlineFontFamilyMedium" android:textSize="18sp" android:paddingBottom="6dp" android:gravity="center"/> <!-- Artist name --> <TextView android:id="@+id/header_artist" android:layout_width="wrap_content" android:layout_height="wrap_content" android:fontFamily="@*android:string/config_bodyFontFamily" android:textSize="14sp" android:singleLine="true" /> </LinearLayout> <!-- Controls --> <LinearLayout android:id="@+id/media_actions" android:orientation="horizontal" android:layoutDirection="ltr" android:layout_width="wrap_content" android:layout_height="match_parent" android:gravity="center" android:layout_gravity="center" > <ImageButton style="@android:style/Widget.Material.Button.Borderless.Small" android:layout_width="48dp" android:layout_height="48dp" android:gravity="center" android:visibility="gone" android:id="@+id/action0" /> <ImageButton style="@android:style/Widget.Material.Button.Borderless.Small" android:layout_width="48dp" android:layout_height="48dp" android:gravity="center" android:visibility="gone" android:id="@+id/action1" /> <ImageButton style="@android:style/Widget.Material.Button.Borderless.Small" android:layout_width="48dp" android:layout_height="48dp" android:gravity="center" android:visibility="gone" android:id="@+id/action2" /> </LinearLayout> </LinearLayout> </com.android.systemui.statusbar.notification.stack.MediaHeaderView>
packages/SystemUI/src/com/android/keyguard/KeyguardMediaPlayer.java 0 → 100644 +266 −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.app.Notification; import android.app.PendingIntent; import android.content.Context; import android.content.res.ColorStateList; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; import android.media.MediaMetadata; import android.util.Log; import android.view.View; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.TextView; import androidx.core.graphics.drawable.RoundedBitmapDrawable; import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory; import androidx.palette.graphics.Palette; import com.android.internal.util.ContrastColorUtil; import com.android.systemui.R; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.statusbar.notification.MediaNotificationProcessor; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.stack.MediaHeaderView; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Singleton; /** * Media controls to display on the lockscreen * * TODO: Should extend MediaControlPanel to avoid code duplication. * Unfortunately, it isn't currently possible because the ActivatableNotificationView background is * different. */ @Singleton public class KeyguardMediaPlayer { private static final String TAG = "KeyguardMediaPlayer"; // Buttons that can be displayed on lock screen media controls. private static final int[] ACTION_IDS = {R.id.action0, R.id.action1, R.id.action2}; private final Context mContext; private final Executor mBackgroundExecutor; private float mAlbumArtRadius; private int mAlbumArtSize; private View mMediaNotifView; @Inject public KeyguardMediaPlayer(Context context, @Background Executor backgroundExecutor) { mContext = context; mBackgroundExecutor = backgroundExecutor; loadDimens(); } /** Binds media controls to a view hierarchy. */ public void bindView(View v) { if (mMediaNotifView != null) { throw new IllegalStateException("cannot bind views, already bound"); } mMediaNotifView = v; loadDimens(); } /** Unbinds media controls. */ public void unbindView() { if (mMediaNotifView == null) { throw new IllegalStateException("cannot unbind views, nothing bound"); } mMediaNotifView = null; } /** Clear the media controls because there isn't an active session. */ public void clearControls() { if (mMediaNotifView != null) { mMediaNotifView.setVisibility(View.GONE); } } /** * Update the media player * * TODO: consider registering a MediaLister instead of exposing this update method. * * @param entry Media notification that will be used to update the player * @param appIcon Icon for the app playing the media * @param mediaMetadata Media metadata that will be used to update the player */ public void updateControls(NotificationEntry entry, Icon appIcon, MediaMetadata mediaMetadata) { if (mMediaNotifView == null) { throw new IllegalStateException("cannot update controls, views not bound"); } if (mediaMetadata == null) { throw new IllegalArgumentException("media metadata was null"); } mMediaNotifView.setVisibility(View.VISIBLE); Notification notif = entry.getSbn().getNotification(); // 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)); } // App icon ImageView appIconView = mMediaNotifView.findViewById(R.id.icon); if (appIconView != null) { Drawable iconDrawable = appIcon.loadDrawable(mContext); iconDrawable.setTint(fgColor); appIconView.setImageDrawable(iconDrawable); } // 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); } // 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); } // 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); } } 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; } 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); } }); } } } } /** * 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); } }
packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java +17 −1 Original line number Diff line number Diff line Loading @@ -46,6 +46,7 @@ import android.widget.ImageView; import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; import com.android.internal.statusbar.NotificationVisibility; import com.android.keyguard.KeyguardMediaPlayer; import com.android.systemui.Dependency; import com.android.systemui.Dumpable; import com.android.systemui.Interpolators; Loading @@ -65,6 +66,7 @@ import com.android.systemui.statusbar.phone.ScrimState; import com.android.systemui.statusbar.phone.StatusBar; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.util.DeviceConfigProxy; import com.android.systemui.util.Utils; import java.io.FileDescriptor; import java.io.PrintWriter; Loading Loading @@ -111,6 +113,7 @@ public class NotificationMediaManager implements Dumpable { private ScrimController mScrimController; @Nullable private LockscreenWallpaper mLockscreenWallpaper; private final KeyguardMediaPlayer mMediaPlayer; private final Executor mMainExecutor; Loading Loading @@ -184,11 +187,13 @@ public class NotificationMediaManager implements Dumpable { NotificationEntryManager notificationEntryManager, MediaArtworkProcessor mediaArtworkProcessor, KeyguardBypassController keyguardBypassController, KeyguardMediaPlayer keyguardMediaPlayer, @Main Executor mainExecutor, DeviceConfigProxy deviceConfig) { mContext = context; mMediaArtworkProcessor = mediaArtworkProcessor; mKeyguardBypassController = keyguardBypassController; mMediaPlayer = keyguardMediaPlayer; mMediaListeners = new ArrayList<>(); // TODO: use MediaSessionManager.SessionListener to hook us up to future updates // in session state Loading Loading @@ -468,6 +473,7 @@ public class NotificationMediaManager implements Dumpable { && mBiometricUnlockController.isWakeAndUnlock(); if (mKeyguardStateController.isLaunchTransitionFadingAway() || wakeAndUnlock) { mBackdrop.setVisibility(View.INVISIBLE); mMediaPlayer.clearControls(); Trace.endSection(); return; } Loading @@ -490,6 +496,14 @@ public class NotificationMediaManager implements Dumpable { } } NotificationEntry entry = mEntryManager .getActiveNotificationUnfiltered(mMediaNotificationKey); if (entry != null) { mMediaPlayer.updateControls(entry, getMediaIcon(), mediaMetadata); } else { mMediaPlayer.clearControls(); } // Process artwork on a background thread and send the resulting bitmap to // finishUpdateMediaMetaData. if (metaDataChanged) { Loading @@ -498,7 +512,7 @@ public class NotificationMediaManager implements Dumpable { } mProcessArtworkTasks.clear(); } if (artworkBitmap != null) { if (artworkBitmap != null && !Utils.useQsMediaPlayer(mContext)) { mProcessArtworkTasks.add(new ProcessArtworkTask(this, metaDataChanged, allowEnterAnimation).execute(artworkBitmap)); } else { Loading Loading @@ -612,6 +626,7 @@ public class NotificationMediaManager implements Dumpable { // We are unlocking directly - no animation! mBackdrop.setVisibility(View.GONE); mBackdropBack.setImageDrawable(null); mMediaPlayer.clearControls(); if (windowController != null) { windowController.setBackdropShowing(false); } Loading @@ -628,6 +643,7 @@ public class NotificationMediaManager implements Dumpable { mBackdrop.setVisibility(View.GONE); mBackdropFront.animate().cancel(); mBackdropBack.setImageDrawable(null); mMediaPlayer.clearControls(); mMainExecutor.execute(mHideBackdropFront); }); if (mKeyguardStateController.isKeyguardFadingAway()) { Loading
packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java +3 −0 Original line number Diff line number Diff line Loading @@ -21,6 +21,7 @@ import android.content.Context; import android.os.Handler; import com.android.internal.statusbar.IStatusBarService; import com.android.keyguard.KeyguardMediaPlayer; import com.android.systemui.bubbles.BubbleController; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.plugins.statusbar.StatusBarStateController; Loading Loading @@ -93,6 +94,7 @@ public interface StatusBarDependenciesModule { NotificationEntryManager notificationEntryManager, MediaArtworkProcessor mediaArtworkProcessor, KeyguardBypassController keyguardBypassController, KeyguardMediaPlayer keyguardMediaPlayer, @Main Executor mainExecutor, DeviceConfigProxy deviceConfigProxy) { return new NotificationMediaManager( Loading @@ -102,6 +104,7 @@ public interface StatusBarDependenciesModule { notificationEntryManager, mediaArtworkProcessor, keyguardBypassController, keyguardMediaPlayer, mainExecutor, deviceConfigProxy); } Loading
packages/SystemUI/src/com/android/systemui/statusbar/notification/MediaNotificationProcessor.java +14 −7 Original line number Diff line number Diff line Loading @@ -152,7 +152,13 @@ public class MediaNotificationProcessor { } } private int selectForegroundColor(int backgroundColor, Palette palette) { /** * Select a foreground color depending on whether the background color is dark or light * @param backgroundColor Background color to coordinate with * @param palette Artwork palette, should be obtained from {@link generateArtworkPaletteBuilder} * @return foreground color */ public static int selectForegroundColor(int backgroundColor, Palette palette) { if (ContrastColorUtil.isColorLight(backgroundColor)) { return selectForegroundColorForSwatches(palette.getDarkVibrantSwatch(), palette.getVibrantSwatch(), Loading @@ -170,7 +176,7 @@ public class MediaNotificationProcessor { } } private int selectForegroundColorForSwatches(Palette.Swatch moreVibrant, private static int selectForegroundColorForSwatches(Palette.Swatch moreVibrant, Palette.Swatch vibrant, Palette.Swatch moreMutedSwatch, Palette.Swatch mutedSwatch, Palette.Swatch dominantSwatch, int fallbackColor) { Palette.Swatch coloredCandidate = selectVibrantCandidate(moreVibrant, vibrant); Loading @@ -194,7 +200,7 @@ public class MediaNotificationProcessor { } } private Palette.Swatch selectMutedCandidate(Palette.Swatch first, private static Palette.Swatch selectMutedCandidate(Palette.Swatch first, Palette.Swatch second) { boolean firstValid = hasEnoughPopulation(first); boolean secondValid = hasEnoughPopulation(second); Loading @@ -215,7 +221,8 @@ public class MediaNotificationProcessor { return null; } private Palette.Swatch selectVibrantCandidate(Palette.Swatch first, Palette.Swatch second) { private static Palette.Swatch selectVibrantCandidate(Palette.Swatch first, Palette.Swatch second) { boolean firstValid = hasEnoughPopulation(first); boolean secondValid = hasEnoughPopulation(second); if (firstValid && secondValid) { Loading @@ -235,7 +242,7 @@ public class MediaNotificationProcessor { return null; } private boolean hasEnoughPopulation(Palette.Swatch swatch) { private static boolean hasEnoughPopulation(Palette.Swatch swatch) { // We want a fraction that is at least 1% of the image return swatch != null && (swatch.getPopulation() / (float) RESIZE_BITMAP_AREA > MINIMUM_IMAGE_FRACTION); Loading @@ -257,7 +264,7 @@ public class MediaNotificationProcessor { * @param palette Artwork palette, should be obtained from {@link generateArtworkPaletteBuilder} * @return Swatch that should be used as the background of the media notification. */ private static Palette.Swatch findBackgroundSwatch(Palette palette) { public static Palette.Swatch findBackgroundSwatch(Palette palette) { // by default we use the dominant palette Palette.Swatch dominantSwatch = palette.getDominantSwatch(); if (dominantSwatch == null) { Loading Loading @@ -301,7 +308,7 @@ public class MediaNotificationProcessor { * @param artwork Media artwork * @return Builder that generates the {@link Palette} for the media artwork. */ private static Palette.Builder generateArtworkPaletteBuilder(Bitmap artwork) { public static Palette.Builder generateArtworkPaletteBuilder(Bitmap artwork) { // for the background we only take the left side of the image to ensure // a smooth transition return Palette.from(artwork) Loading