Merge "Merge "Compute background color." into qt-dev am: a3fb334c6f" into qt-dev-plus-aosp
This commit is contained in:
committed by
Android (Google) Code Review
commit
237e648e43
@@ -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
|
||||
@@ -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
|
||||
@@ -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!!
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -449,28 +454,37 @@ 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
|
||||
}
|
||||
if (artworkBitmap != null) {
|
||||
int notificationColor;
|
||||
synchronized (mEntryManager.getNotificationData()) {
|
||||
NotificationEntry entry = mEntryManager.getNotificationData()
|
||||
.get(mMediaNotificationKey);
|
||||
if (entry == null || entry.getRow() == null) {
|
||||
notificationColor = Color.TRANSPARENT;
|
||||
} else {
|
||||
notificationColor = entry.getRow().calculateBgColor();
|
||||
}
|
||||
}
|
||||
Bitmap bmp = mMediaArtworkProcessor.processArtwork(mContext, artworkBitmap,
|
||||
notificationColor);
|
||||
artworkDrawable = new BitmapDrawable(mBackdropBack.getResources(), bmp);
|
||||
}
|
||||
|
||||
// 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) {
|
||||
mProcessArtworkTasks.add(new ProcessArtworkTask(this, metaDataChanged,
|
||||
allowEnterAnimation).execute(artworkBitmap));
|
||||
} else {
|
||||
finishUpdateMediaMetaData(metaDataChanged, allowEnterAnimation, null);
|
||||
}
|
||||
|
||||
Trace.endSection();
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -598,7 +612,6 @@ public class NotificationMediaManager implements Dumpable {
|
||||
}
|
||||
}
|
||||
}
|
||||
Trace.endSection();
|
||||
}
|
||||
|
||||
public void setup(BackDropView backdrop, ImageView backdropFront, ImageView backdropBack,
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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() {
|
||||
@@ -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(
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user