Loading packages/SystemUI/src/com/android/systemui/statusbar/MediaArtworkProcessor.kt +5 −3 Original line number Diff line number Diff line Loading @@ -16,7 +16,6 @@ package com.android.systemui.statusbar import android.annotation.ColorInt import android.content.Context import android.graphics.Bitmap import android.graphics.Canvas Loading @@ -28,6 +27,7 @@ import android.renderscript.RenderScript import android.renderscript.ScriptIntrinsicBlur import android.util.MathUtils import com.android.internal.graphics.ColorUtils import com.android.systemui.statusbar.notification.MediaNotificationProcessor import javax.inject.Inject import javax.inject.Singleton Loading @@ -42,7 +42,7 @@ class MediaArtworkProcessor @Inject constructor() { private val mTmpSize = Point() private var mArtworkCache: Bitmap? = null fun processArtwork(context: Context, artwork: Bitmap, @ColorInt color: Int): Bitmap { fun processArtwork(context: Context, artwork: Bitmap): Bitmap { if (mArtworkCache != null) { return mArtworkCache!! } Loading Loading @@ -71,13 +71,15 @@ class MediaArtworkProcessor @Inject constructor() { blur.forEach(output) output.copyTo(outBitmap) val swatch = MediaNotificationProcessor.findBackgroundSwatch(artwork) input.destroy() output.destroy() inBitmap.recycle() blur.destroy() val canvas = Canvas(outBitmap) canvas.drawColor(ColorUtils.setAlphaComponent(color, COLOR_ALPHA)) canvas.drawColor(ColorUtils.setAlphaComponent(swatch.rgb, COLOR_ALPHA)) return outBitmap } Loading packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java +87 −19 Original line number Diff line number Diff line Loading @@ -21,11 +21,11 @@ import static com.android.systemui.statusbar.phone.StatusBar.DEBUG_MEDIA_FAKE_AR import static com.android.systemui.statusbar.phone.StatusBar.ENABLE_LOCKSCREEN_WALLPAPER; import static com.android.systemui.statusbar.phone.StatusBar.SHOW_LOCKSCREEN_MEDIA_ARTWORK; import android.annotation.MainThread; import android.annotation.Nullable; import android.app.Notification; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Color; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; Loading @@ -35,11 +35,13 @@ import android.media.session.MediaController; import android.media.session.MediaSession; import android.media.session.MediaSessionManager; import android.media.session.PlaybackState; import android.os.AsyncTask; import android.os.Handler; import android.os.Trace; import android.os.UserHandle; import android.provider.DeviceConfig; import android.provider.DeviceConfig.Properties; import android.util.ArraySet; import android.util.Log; import android.view.View; import android.widget.ImageView; Loading @@ -64,8 +66,10 @@ import com.android.systemui.statusbar.policy.KeyguardMonitor; import java.io.FileDescriptor; import java.io.PrintWriter; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; import java.util.Set; import javax.inject.Inject; import javax.inject.Singleton; Loading Loading @@ -108,6 +112,7 @@ public class NotificationMediaManager implements Dumpable { private final MediaSessionManager mMediaSessionManager; private final ArrayList<MediaListener> mMediaListeners; private final MediaArtworkProcessor mMediaArtworkProcessor; private final Set<AsyncTask<?, ?, ?>> mProcessArtworkTasks = new ArraySet<>(); protected NotificationPresenter mPresenter; private MediaController mMediaController; Loading Loading @@ -449,29 +454,38 @@ public class NotificationMediaManager implements Dumpable { + " state=" + mStatusBarStateController.getState()); } Drawable artworkDrawable = null; Bitmap artworkBitmap = null; if (mediaMetadata != null) { Bitmap artworkBitmap = mediaMetadata.getBitmap(MediaMetadata.METADATA_KEY_ART); artworkBitmap = mediaMetadata.getBitmap(MediaMetadata.METADATA_KEY_ART); if (artworkBitmap == null) { artworkBitmap = mediaMetadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART); // might still be null } } // Process artwork on a background thread and send the resulting bitmap to // finishUpdateMediaMetaData. if (metaDataChanged) { for (AsyncTask<?, ?, ?> task : mProcessArtworkTasks) { task.cancel(true); } mProcessArtworkTasks.clear(); } if (artworkBitmap != null) { int notificationColor; synchronized (mEntryManager.getNotificationData()) { NotificationEntry entry = mEntryManager.getNotificationData() .get(mMediaNotificationKey); if (entry == null || entry.getRow() == null) { notificationColor = Color.TRANSPARENT; mProcessArtworkTasks.add(new ProcessArtworkTask(this, metaDataChanged, allowEnterAnimation).execute(artworkBitmap)); } else { notificationColor = entry.getRow().calculateBgColor(); finishUpdateMediaMetaData(metaDataChanged, allowEnterAnimation, null); } Trace.endSection(); } Bitmap bmp = mMediaArtworkProcessor.processArtwork(mContext, artworkBitmap, notificationColor); private void finishUpdateMediaMetaData(boolean metaDataChanged, boolean allowEnterAnimation, @Nullable Bitmap bmp) { Drawable artworkDrawable = null; if (bmp != null) { artworkDrawable = new BitmapDrawable(mBackdropBack.getResources(), bmp); } } boolean allowWhenShade = false; if (ENABLE_LOCKSCREEN_WALLPAPER && artworkDrawable == null) { Bitmap lockWallpaper = Loading Loading @@ -598,7 +612,6 @@ public class NotificationMediaManager implements Dumpable { } } } Trace.endSection(); } public void setup(BackDropView backdrop, ImageView backdropFront, ImageView backdropBack, Loading Loading @@ -629,6 +642,61 @@ public class NotificationMediaManager implements Dumpable { } }; private Bitmap processArtwork(Bitmap artwork) { return mMediaArtworkProcessor.processArtwork(mContext, artwork); } @MainThread private void removeTask(AsyncTask<?, ?, ?> task) { mProcessArtworkTasks.remove(task); } /** * {@link AsyncTask} to prepare album art for use as backdrop on lock screen. */ private static final class ProcessArtworkTask extends AsyncTask<Bitmap, Void, Bitmap> { private final WeakReference<NotificationMediaManager> mManagerRef; private final boolean mMetaDataChanged; private final boolean mAllowEnterAnimation; ProcessArtworkTask(NotificationMediaManager manager, boolean changed, boolean allowAnimation) { mManagerRef = new WeakReference<>(manager); mMetaDataChanged = changed; mAllowEnterAnimation = allowAnimation; } @Override protected Bitmap doInBackground(Bitmap... bitmaps) { NotificationMediaManager manager = mManagerRef.get(); if (manager == null || bitmaps.length == 0 || isCancelled()) { return null; } return manager.processArtwork(bitmaps[0]); } @Override protected void onPostExecute(@Nullable Bitmap result) { NotificationMediaManager manager = mManagerRef.get(); if (manager != null && !isCancelled()) { manager.removeTask(this); manager.finishUpdateMediaMetaData(mMetaDataChanged, mAllowEnterAnimation, result); } } @Override protected void onCancelled(Bitmap result) { if (result != null) { result.recycle(); } NotificationMediaManager manager = mManagerRef.get(); if (manager != null) { manager.removeTask(this); } } } public interface MediaListener { void onMetadataChanged(MediaMetadata metadata); } Loading packages/SystemUI/src/com/android/systemui/statusbar/notification/MediaNotificationProcessor.java +51 −28 Original line number Diff line number Diff line Loading @@ -69,8 +69,7 @@ public class MediaNotificationProcessor { private static final int RESIZE_BITMAP_AREA = 150 * 150; private final ImageGradientColorizer mColorizer; private final Context mContext; private float[] mFilteredBackgroundHsl = null; private Palette.Filter mBlackWhiteFilter = (rgb, hsl) -> !isWhiteOrBlack(hsl); private final Palette.Filter mBlackWhiteFilter = (rgb, hsl) -> !isWhiteOrBlack(hsl); /** * The context of the notification. This is the app context of the package posting the Loading Loading @@ -121,23 +120,21 @@ public class MediaNotificationProcessor { drawable.setBounds(0, 0, width, height); drawable.draw(canvas); // for the background we only take the left side of the image to ensure // a smooth transition Palette.Builder paletteBuilder = Palette.from(bitmap) .setRegion(0, 0, bitmap.getWidth() / 2, bitmap.getHeight()) .clearFilters() // we want all colors, red / white / black ones too! .resizeBitmapArea(RESIZE_BITMAP_AREA); Palette.Builder paletteBuilder = generateArtworkPaletteBuilder(bitmap); Palette palette = paletteBuilder.generate(); backgroundColor = findBackgroundColorAndFilter(palette); Palette.Swatch backgroundSwatch = findBackgroundSwatch(palette); backgroundColor = backgroundSwatch.getRgb(); // we want most of the full region again, slightly shifted to the right float textColorStartWidthFraction = 0.4f; paletteBuilder.setRegion((int) (bitmap.getWidth() * textColorStartWidthFraction), 0, bitmap.getWidth(), bitmap.getHeight()); if (mFilteredBackgroundHsl != null) { // We're not filtering on white or black if (!isWhiteOrBlack(backgroundSwatch.getHsl())) { final float backgroundHue = backgroundSwatch.getHsl()[0]; paletteBuilder.addFilter((rgb, hsl) -> { // at least 10 degrees hue difference float diff = Math.abs(hsl[0] - mFilteredBackgroundHsl[0]); float diff = Math.abs(hsl[0] - backgroundHue); return diff > 10 && diff < 350; }); } Loading Loading @@ -244,18 +241,31 @@ public class MediaNotificationProcessor { && (swatch.getPopulation() / (float) RESIZE_BITMAP_AREA > MINIMUM_IMAGE_FRACTION); } private int findBackgroundColorAndFilter(Palette palette) { /** * Finds an appropriate background swatch from media artwork. * * @param artwork Media artwork * @return Swatch that should be used as the background of the media notification. */ public static Palette.Swatch findBackgroundSwatch(Bitmap artwork) { return findBackgroundSwatch(generateArtworkPaletteBuilder(artwork).generate()); } /** * Finds an appropriate background swatch from the palette of media artwork. * * @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) { // by default we use the dominant palette Palette.Swatch dominantSwatch = palette.getDominantSwatch(); if (dominantSwatch == null) { // We're not filtering on white or black mFilteredBackgroundHsl = null; return Color.WHITE; return new Palette.Swatch(Color.WHITE, 100); } if (!isWhiteOrBlack(dominantSwatch.getHsl())) { mFilteredBackgroundHsl = dominantSwatch.getHsl(); return dominantSwatch.getRgb(); return dominantSwatch; } // Oh well, we selected black or white. Lets look at the second color! List<Palette.Swatch> swatches = palette.getSwatches(); Loading @@ -270,38 +280,51 @@ public class MediaNotificationProcessor { } } if (second == null) { // We're not filtering on white or black mFilteredBackgroundHsl = null; return dominantSwatch.getRgb(); return dominantSwatch; } if (dominantSwatch.getPopulation() / highestNonWhitePopulation > POPULATION_FRACTION_FOR_WHITE_OR_BLACK) { // The dominant swatch is very dominant, lets take it! // We're not filtering on white or black mFilteredBackgroundHsl = null; return dominantSwatch.getRgb(); return dominantSwatch; } else { mFilteredBackgroundHsl = second.getHsl(); return second.getRgb(); return second; } } private boolean isWhiteOrBlack(float[] hsl) { return isBlack(hsl) || isWhite(hsl); /** * Generate a palette builder for media artwork. * * For producing a smooth background transition, the palette is extracted from only the left * side of the artwork. * * @param artwork Media artwork * @return Builder that generates the {@link Palette} for the media artwork. */ private 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) .setRegion(0, 0, artwork.getWidth() / 2, artwork.getHeight()) .clearFilters() // we want all colors, red / white / black ones too! .resizeBitmapArea(RESIZE_BITMAP_AREA); } private static boolean isWhiteOrBlack(float[] hsl) { return isBlack(hsl) || isWhite(hsl); } /** * @return true if the color represents a color which is close to black. */ private boolean isBlack(float[] hslColor) { private static boolean isBlack(float[] hslColor) { return hslColor[2] <= BLACK_MAX_LIGHTNESS; } /** * @return true if the color represents a color which is close to white. */ private boolean isWhite(float[] hslColor) { private static boolean isWhite(float[] hslColor) { return hslColor[2] >= WHITE_MIN_LIGHTNESS; } } packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/MediaNotificationProcessorTest.java +54 −0 Original line number Diff line number Diff line Loading @@ -16,6 +16,8 @@ package com.android.systemui.statusbar.notification; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertNotSame; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; Loading @@ -24,17 +26,22 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; import android.annotation.Nullable; import android.app.Notification; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.drawable.Drawable; import android.test.suitebuilder.annotation.SmallTest; import android.widget.RemoteViews; import androidx.palette.graphics.Palette; import androidx.test.runner.AndroidJUnit4; import com.android.systemui.R; import com.android.systemui.SysuiTestCase; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; Loading @@ -43,9 +50,18 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public class MediaNotificationProcessorTest extends SysuiTestCase { private static final int BITMAP_WIDTH = 10; private static final int BITMAP_HEIGHT = 10; /** * Color tolerance is borrowed from the AndroidX test utilities for Palette. */ private static final int COLOR_TOLERANCE = 8; private MediaNotificationProcessor mProcessor; private Bitmap mBitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); private ImageGradientColorizer mColorizer; @Nullable private Bitmap mArtwork; @Before public void setUp() { Loading @@ -53,6 +69,14 @@ public class MediaNotificationProcessorTest extends SysuiTestCase { mProcessor = new MediaNotificationProcessor(getContext(), getContext(), mColorizer); } @After public void tearDown() { if (mArtwork != null) { mArtwork.recycle(); mArtwork = null; } } @Test public void testColorizedWithLargeIcon() { Notification.Builder builder = new Notification.Builder(getContext()).setSmallIcon( Loading Loading @@ -100,6 +124,36 @@ public class MediaNotificationProcessorTest extends SysuiTestCase { assertNotSame(contentView, remoteViews); } @Test public void findBackgroundSwatch_white() { // Given artwork that is completely white. mArtwork = Bitmap.createBitmap(BITMAP_WIDTH, BITMAP_HEIGHT, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(mArtwork); canvas.drawColor(Color.WHITE); // WHEN the background swatch is computed Palette.Swatch swatch = MediaNotificationProcessor.findBackgroundSwatch(mArtwork); // THEN the swatch color is white assertCloseColors(swatch.getRgb(), Color.WHITE); } @Test public void findBackgroundSwatch_red() { // Given artwork that is completely red. mArtwork = Bitmap.createBitmap(BITMAP_WIDTH, BITMAP_HEIGHT, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(mArtwork); canvas.drawColor(Color.RED); // WHEN the background swatch is computed Palette.Swatch swatch = MediaNotificationProcessor.findBackgroundSwatch(mArtwork); // THEN the swatch color is red assertCloseColors(swatch.getRgb(), Color.RED); } static void assertCloseColors(int expected, int actual) { assertThat((float) Color.red(expected)).isWithin(COLOR_TOLERANCE).of(Color.red(actual)); assertThat((float) Color.green(expected)).isWithin(COLOR_TOLERANCE).of(Color.green(actual)); assertThat((float) Color.blue(expected)).isWithin(COLOR_TOLERANCE).of(Color.blue(actual)); } public static class TestableColorizer extends ImageGradientColorizer { private final Bitmap mBitmap; Loading Loading
packages/SystemUI/src/com/android/systemui/statusbar/MediaArtworkProcessor.kt +5 −3 Original line number Diff line number Diff line Loading @@ -16,7 +16,6 @@ package com.android.systemui.statusbar import android.annotation.ColorInt import android.content.Context import android.graphics.Bitmap import android.graphics.Canvas Loading @@ -28,6 +27,7 @@ import android.renderscript.RenderScript import android.renderscript.ScriptIntrinsicBlur import android.util.MathUtils import com.android.internal.graphics.ColorUtils import com.android.systemui.statusbar.notification.MediaNotificationProcessor import javax.inject.Inject import javax.inject.Singleton Loading @@ -42,7 +42,7 @@ class MediaArtworkProcessor @Inject constructor() { private val mTmpSize = Point() private var mArtworkCache: Bitmap? = null fun processArtwork(context: Context, artwork: Bitmap, @ColorInt color: Int): Bitmap { fun processArtwork(context: Context, artwork: Bitmap): Bitmap { if (mArtworkCache != null) { return mArtworkCache!! } Loading Loading @@ -71,13 +71,15 @@ class MediaArtworkProcessor @Inject constructor() { blur.forEach(output) output.copyTo(outBitmap) val swatch = MediaNotificationProcessor.findBackgroundSwatch(artwork) input.destroy() output.destroy() inBitmap.recycle() blur.destroy() val canvas = Canvas(outBitmap) canvas.drawColor(ColorUtils.setAlphaComponent(color, COLOR_ALPHA)) canvas.drawColor(ColorUtils.setAlphaComponent(swatch.rgb, COLOR_ALPHA)) return outBitmap } Loading
packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java +87 −19 Original line number Diff line number Diff line Loading @@ -21,11 +21,11 @@ import static com.android.systemui.statusbar.phone.StatusBar.DEBUG_MEDIA_FAKE_AR import static com.android.systemui.statusbar.phone.StatusBar.ENABLE_LOCKSCREEN_WALLPAPER; import static com.android.systemui.statusbar.phone.StatusBar.SHOW_LOCKSCREEN_MEDIA_ARTWORK; import android.annotation.MainThread; import android.annotation.Nullable; import android.app.Notification; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Color; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; Loading @@ -35,11 +35,13 @@ import android.media.session.MediaController; import android.media.session.MediaSession; import android.media.session.MediaSessionManager; import android.media.session.PlaybackState; import android.os.AsyncTask; import android.os.Handler; import android.os.Trace; import android.os.UserHandle; import android.provider.DeviceConfig; import android.provider.DeviceConfig.Properties; import android.util.ArraySet; import android.util.Log; import android.view.View; import android.widget.ImageView; Loading @@ -64,8 +66,10 @@ import com.android.systemui.statusbar.policy.KeyguardMonitor; import java.io.FileDescriptor; import java.io.PrintWriter; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; import java.util.Set; import javax.inject.Inject; import javax.inject.Singleton; Loading Loading @@ -108,6 +112,7 @@ public class NotificationMediaManager implements Dumpable { private final MediaSessionManager mMediaSessionManager; private final ArrayList<MediaListener> mMediaListeners; private final MediaArtworkProcessor mMediaArtworkProcessor; private final Set<AsyncTask<?, ?, ?>> mProcessArtworkTasks = new ArraySet<>(); protected NotificationPresenter mPresenter; private MediaController mMediaController; Loading Loading @@ -449,29 +454,38 @@ public class NotificationMediaManager implements Dumpable { + " state=" + mStatusBarStateController.getState()); } Drawable artworkDrawable = null; Bitmap artworkBitmap = null; if (mediaMetadata != null) { Bitmap artworkBitmap = mediaMetadata.getBitmap(MediaMetadata.METADATA_KEY_ART); artworkBitmap = mediaMetadata.getBitmap(MediaMetadata.METADATA_KEY_ART); if (artworkBitmap == null) { artworkBitmap = mediaMetadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART); // might still be null } } // Process artwork on a background thread and send the resulting bitmap to // finishUpdateMediaMetaData. if (metaDataChanged) { for (AsyncTask<?, ?, ?> task : mProcessArtworkTasks) { task.cancel(true); } mProcessArtworkTasks.clear(); } if (artworkBitmap != null) { int notificationColor; synchronized (mEntryManager.getNotificationData()) { NotificationEntry entry = mEntryManager.getNotificationData() .get(mMediaNotificationKey); if (entry == null || entry.getRow() == null) { notificationColor = Color.TRANSPARENT; mProcessArtworkTasks.add(new ProcessArtworkTask(this, metaDataChanged, allowEnterAnimation).execute(artworkBitmap)); } else { notificationColor = entry.getRow().calculateBgColor(); finishUpdateMediaMetaData(metaDataChanged, allowEnterAnimation, null); } Trace.endSection(); } Bitmap bmp = mMediaArtworkProcessor.processArtwork(mContext, artworkBitmap, notificationColor); private void finishUpdateMediaMetaData(boolean metaDataChanged, boolean allowEnterAnimation, @Nullable Bitmap bmp) { Drawable artworkDrawable = null; if (bmp != null) { artworkDrawable = new BitmapDrawable(mBackdropBack.getResources(), bmp); } } boolean allowWhenShade = false; if (ENABLE_LOCKSCREEN_WALLPAPER && artworkDrawable == null) { Bitmap lockWallpaper = Loading Loading @@ -598,7 +612,6 @@ public class NotificationMediaManager implements Dumpable { } } } Trace.endSection(); } public void setup(BackDropView backdrop, ImageView backdropFront, ImageView backdropBack, Loading Loading @@ -629,6 +642,61 @@ public class NotificationMediaManager implements Dumpable { } }; private Bitmap processArtwork(Bitmap artwork) { return mMediaArtworkProcessor.processArtwork(mContext, artwork); } @MainThread private void removeTask(AsyncTask<?, ?, ?> task) { mProcessArtworkTasks.remove(task); } /** * {@link AsyncTask} to prepare album art for use as backdrop on lock screen. */ private static final class ProcessArtworkTask extends AsyncTask<Bitmap, Void, Bitmap> { private final WeakReference<NotificationMediaManager> mManagerRef; private final boolean mMetaDataChanged; private final boolean mAllowEnterAnimation; ProcessArtworkTask(NotificationMediaManager manager, boolean changed, boolean allowAnimation) { mManagerRef = new WeakReference<>(manager); mMetaDataChanged = changed; mAllowEnterAnimation = allowAnimation; } @Override protected Bitmap doInBackground(Bitmap... bitmaps) { NotificationMediaManager manager = mManagerRef.get(); if (manager == null || bitmaps.length == 0 || isCancelled()) { return null; } return manager.processArtwork(bitmaps[0]); } @Override protected void onPostExecute(@Nullable Bitmap result) { NotificationMediaManager manager = mManagerRef.get(); if (manager != null && !isCancelled()) { manager.removeTask(this); manager.finishUpdateMediaMetaData(mMetaDataChanged, mAllowEnterAnimation, result); } } @Override protected void onCancelled(Bitmap result) { if (result != null) { result.recycle(); } NotificationMediaManager manager = mManagerRef.get(); if (manager != null) { manager.removeTask(this); } } } public interface MediaListener { void onMetadataChanged(MediaMetadata metadata); } Loading
packages/SystemUI/src/com/android/systemui/statusbar/notification/MediaNotificationProcessor.java +51 −28 Original line number Diff line number Diff line Loading @@ -69,8 +69,7 @@ public class MediaNotificationProcessor { private static final int RESIZE_BITMAP_AREA = 150 * 150; private final ImageGradientColorizer mColorizer; private final Context mContext; private float[] mFilteredBackgroundHsl = null; private Palette.Filter mBlackWhiteFilter = (rgb, hsl) -> !isWhiteOrBlack(hsl); private final Palette.Filter mBlackWhiteFilter = (rgb, hsl) -> !isWhiteOrBlack(hsl); /** * The context of the notification. This is the app context of the package posting the Loading Loading @@ -121,23 +120,21 @@ public class MediaNotificationProcessor { drawable.setBounds(0, 0, width, height); drawable.draw(canvas); // for the background we only take the left side of the image to ensure // a smooth transition Palette.Builder paletteBuilder = Palette.from(bitmap) .setRegion(0, 0, bitmap.getWidth() / 2, bitmap.getHeight()) .clearFilters() // we want all colors, red / white / black ones too! .resizeBitmapArea(RESIZE_BITMAP_AREA); Palette.Builder paletteBuilder = generateArtworkPaletteBuilder(bitmap); Palette palette = paletteBuilder.generate(); backgroundColor = findBackgroundColorAndFilter(palette); Palette.Swatch backgroundSwatch = findBackgroundSwatch(palette); backgroundColor = backgroundSwatch.getRgb(); // we want most of the full region again, slightly shifted to the right float textColorStartWidthFraction = 0.4f; paletteBuilder.setRegion((int) (bitmap.getWidth() * textColorStartWidthFraction), 0, bitmap.getWidth(), bitmap.getHeight()); if (mFilteredBackgroundHsl != null) { // We're not filtering on white or black if (!isWhiteOrBlack(backgroundSwatch.getHsl())) { final float backgroundHue = backgroundSwatch.getHsl()[0]; paletteBuilder.addFilter((rgb, hsl) -> { // at least 10 degrees hue difference float diff = Math.abs(hsl[0] - mFilteredBackgroundHsl[0]); float diff = Math.abs(hsl[0] - backgroundHue); return diff > 10 && diff < 350; }); } Loading Loading @@ -244,18 +241,31 @@ public class MediaNotificationProcessor { && (swatch.getPopulation() / (float) RESIZE_BITMAP_AREA > MINIMUM_IMAGE_FRACTION); } private int findBackgroundColorAndFilter(Palette palette) { /** * Finds an appropriate background swatch from media artwork. * * @param artwork Media artwork * @return Swatch that should be used as the background of the media notification. */ public static Palette.Swatch findBackgroundSwatch(Bitmap artwork) { return findBackgroundSwatch(generateArtworkPaletteBuilder(artwork).generate()); } /** * Finds an appropriate background swatch from the palette of media artwork. * * @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) { // by default we use the dominant palette Palette.Swatch dominantSwatch = palette.getDominantSwatch(); if (dominantSwatch == null) { // We're not filtering on white or black mFilteredBackgroundHsl = null; return Color.WHITE; return new Palette.Swatch(Color.WHITE, 100); } if (!isWhiteOrBlack(dominantSwatch.getHsl())) { mFilteredBackgroundHsl = dominantSwatch.getHsl(); return dominantSwatch.getRgb(); return dominantSwatch; } // Oh well, we selected black or white. Lets look at the second color! List<Palette.Swatch> swatches = palette.getSwatches(); Loading @@ -270,38 +280,51 @@ public class MediaNotificationProcessor { } } if (second == null) { // We're not filtering on white or black mFilteredBackgroundHsl = null; return dominantSwatch.getRgb(); return dominantSwatch; } if (dominantSwatch.getPopulation() / highestNonWhitePopulation > POPULATION_FRACTION_FOR_WHITE_OR_BLACK) { // The dominant swatch is very dominant, lets take it! // We're not filtering on white or black mFilteredBackgroundHsl = null; return dominantSwatch.getRgb(); return dominantSwatch; } else { mFilteredBackgroundHsl = second.getHsl(); return second.getRgb(); return second; } } private boolean isWhiteOrBlack(float[] hsl) { return isBlack(hsl) || isWhite(hsl); /** * Generate a palette builder for media artwork. * * For producing a smooth background transition, the palette is extracted from only the left * side of the artwork. * * @param artwork Media artwork * @return Builder that generates the {@link Palette} for the media artwork. */ private 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) .setRegion(0, 0, artwork.getWidth() / 2, artwork.getHeight()) .clearFilters() // we want all colors, red / white / black ones too! .resizeBitmapArea(RESIZE_BITMAP_AREA); } private static boolean isWhiteOrBlack(float[] hsl) { return isBlack(hsl) || isWhite(hsl); } /** * @return true if the color represents a color which is close to black. */ private boolean isBlack(float[] hslColor) { private static boolean isBlack(float[] hslColor) { return hslColor[2] <= BLACK_MAX_LIGHTNESS; } /** * @return true if the color represents a color which is close to white. */ private boolean isWhite(float[] hslColor) { private static boolean isWhite(float[] hslColor) { return hslColor[2] >= WHITE_MIN_LIGHTNESS; } }
packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/MediaNotificationProcessorTest.java +54 −0 Original line number Diff line number Diff line Loading @@ -16,6 +16,8 @@ package com.android.systemui.statusbar.notification; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertNotSame; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; Loading @@ -24,17 +26,22 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; import android.annotation.Nullable; import android.app.Notification; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.drawable.Drawable; import android.test.suitebuilder.annotation.SmallTest; import android.widget.RemoteViews; import androidx.palette.graphics.Palette; import androidx.test.runner.AndroidJUnit4; import com.android.systemui.R; import com.android.systemui.SysuiTestCase; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; Loading @@ -43,9 +50,18 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public class MediaNotificationProcessorTest extends SysuiTestCase { private static final int BITMAP_WIDTH = 10; private static final int BITMAP_HEIGHT = 10; /** * Color tolerance is borrowed from the AndroidX test utilities for Palette. */ private static final int COLOR_TOLERANCE = 8; private MediaNotificationProcessor mProcessor; private Bitmap mBitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); private ImageGradientColorizer mColorizer; @Nullable private Bitmap mArtwork; @Before public void setUp() { Loading @@ -53,6 +69,14 @@ public class MediaNotificationProcessorTest extends SysuiTestCase { mProcessor = new MediaNotificationProcessor(getContext(), getContext(), mColorizer); } @After public void tearDown() { if (mArtwork != null) { mArtwork.recycle(); mArtwork = null; } } @Test public void testColorizedWithLargeIcon() { Notification.Builder builder = new Notification.Builder(getContext()).setSmallIcon( Loading Loading @@ -100,6 +124,36 @@ public class MediaNotificationProcessorTest extends SysuiTestCase { assertNotSame(contentView, remoteViews); } @Test public void findBackgroundSwatch_white() { // Given artwork that is completely white. mArtwork = Bitmap.createBitmap(BITMAP_WIDTH, BITMAP_HEIGHT, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(mArtwork); canvas.drawColor(Color.WHITE); // WHEN the background swatch is computed Palette.Swatch swatch = MediaNotificationProcessor.findBackgroundSwatch(mArtwork); // THEN the swatch color is white assertCloseColors(swatch.getRgb(), Color.WHITE); } @Test public void findBackgroundSwatch_red() { // Given artwork that is completely red. mArtwork = Bitmap.createBitmap(BITMAP_WIDTH, BITMAP_HEIGHT, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(mArtwork); canvas.drawColor(Color.RED); // WHEN the background swatch is computed Palette.Swatch swatch = MediaNotificationProcessor.findBackgroundSwatch(mArtwork); // THEN the swatch color is red assertCloseColors(swatch.getRgb(), Color.RED); } static void assertCloseColors(int expected, int actual) { assertThat((float) Color.red(expected)).isWithin(COLOR_TOLERANCE).of(Color.red(actual)); assertThat((float) Color.green(expected)).isWithin(COLOR_TOLERANCE).of(Color.green(actual)); assertThat((float) Color.blue(expected)).isWithin(COLOR_TOLERANCE).of(Color.blue(actual)); } public static class TestableColorizer extends ImageGradientColorizer { private final Bitmap mBitmap; Loading