Extracting the notification colors based on the album art
Media notifications are now extracting the background and foreground colors from the album art. Test: manual, play different songs Bug: 36561228 Change-Id: I9c3c962fa59eb70ef9b2d4893b939be6e1ee1ab0
This commit is contained in:
@@ -2681,6 +2681,8 @@ public class Notification implements Parcelable
|
||||
private int mPrimaryTextColor = COLOR_INVALID;
|
||||
private int mSecondaryTextColor = COLOR_INVALID;
|
||||
private int mActionBarColor = COLOR_INVALID;
|
||||
private int mBackgroundColor = COLOR_INVALID;
|
||||
private int mForegroundColor = COLOR_INVALID;
|
||||
|
||||
/**
|
||||
* Constructs a new Builder with the defaults:
|
||||
@@ -3854,10 +3856,62 @@ public class Notification implements Parcelable
|
||||
|| mActionBarColor == COLOR_INVALID
|
||||
|| mTextColorsAreForBackground != backgroundColor) {
|
||||
mTextColorsAreForBackground = backgroundColor;
|
||||
mPrimaryTextColor = NotificationColorUtil.resolvePrimaryColor(
|
||||
mContext, backgroundColor);
|
||||
mSecondaryTextColor = NotificationColorUtil.resolveSecondaryColor(
|
||||
mContext, backgroundColor);
|
||||
if (mForegroundColor == COLOR_INVALID || !isColorized()) {
|
||||
mPrimaryTextColor = NotificationColorUtil.resolvePrimaryColor(mContext,
|
||||
backgroundColor);
|
||||
mSecondaryTextColor = NotificationColorUtil.resolveSecondaryColor(mContext,
|
||||
backgroundColor);
|
||||
} else {
|
||||
double backLum = NotificationColorUtil.calculateLuminance(backgroundColor);
|
||||
double textLum = NotificationColorUtil.calculateLuminance(mForegroundColor);
|
||||
double contrast = NotificationColorUtil.calculateContrast(mForegroundColor,
|
||||
backgroundColor);
|
||||
boolean textDark = backLum > textLum;
|
||||
if (contrast < 4.5f) {
|
||||
if (textDark) {
|
||||
mSecondaryTextColor = NotificationColorUtil.findContrastColor(
|
||||
mForegroundColor,
|
||||
backgroundColor,
|
||||
true /* findFG */,
|
||||
4.5f);
|
||||
mPrimaryTextColor = NotificationColorUtil.changeColorLightness(
|
||||
mSecondaryTextColor, -20);
|
||||
} else {
|
||||
mSecondaryTextColor =
|
||||
NotificationColorUtil.findContrastColorAgainstDark(
|
||||
mForegroundColor,
|
||||
backgroundColor,
|
||||
true /* findFG */,
|
||||
4.5f);
|
||||
mPrimaryTextColor = NotificationColorUtil.changeColorLightness(
|
||||
mSecondaryTextColor, 10);
|
||||
}
|
||||
} else {
|
||||
mPrimaryTextColor = mForegroundColor;
|
||||
mSecondaryTextColor = NotificationColorUtil.changeColorLightness(
|
||||
mPrimaryTextColor, textDark ? 10 : -20);
|
||||
if (NotificationColorUtil.calculateContrast(mSecondaryTextColor,
|
||||
backgroundColor) < 4.5f) {
|
||||
// oh well the secondary is not good enough
|
||||
if (textDark) {
|
||||
mSecondaryTextColor = NotificationColorUtil.findContrastColor(
|
||||
mSecondaryTextColor,
|
||||
backgroundColor,
|
||||
true /* findFG */,
|
||||
4.5f);
|
||||
} else {
|
||||
mSecondaryTextColor
|
||||
= NotificationColorUtil.findContrastColorAgainstDark(
|
||||
mSecondaryTextColor,
|
||||
backgroundColor,
|
||||
true /* findFG */,
|
||||
4.5f);
|
||||
}
|
||||
mPrimaryTextColor = NotificationColorUtil.changeColorLightness(
|
||||
mSecondaryTextColor, textDark ? -20 : 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
mActionBarColor = NotificationColorUtil.resolveActionBarColor(mContext,
|
||||
backgroundColor);
|
||||
}
|
||||
@@ -4845,7 +4899,7 @@ public class Notification implements Parcelable
|
||||
|
||||
private int getBackgroundColor() {
|
||||
if (isColorized()) {
|
||||
return mN.color;
|
||||
return mBackgroundColor != COLOR_INVALID ? mBackgroundColor : mN.color;
|
||||
} else {
|
||||
return COLOR_DEFAULT;
|
||||
}
|
||||
@@ -4863,6 +4917,21 @@ public class Notification implements Parcelable
|
||||
return targetSdkVersion > Build.VERSION_CODES.M
|
||||
&& targetSdkVersion < Build.VERSION_CODES.O;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a color palette to be used as the background and textColors
|
||||
*
|
||||
* @param backgroundColor the color to be used as the background
|
||||
* @param foregroundColor the color to be used as the foreground
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
public void setColorPalette(int backgroundColor, int foregroundColor) {
|
||||
mBackgroundColor = backgroundColor;
|
||||
mForegroundColor = foregroundColor;
|
||||
mTextColorsAreForBackground = COLOR_INVALID;
|
||||
ensureColors();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -4899,6 +4968,18 @@ public class Notification implements Parcelable
|
||||
* @hide
|
||||
*/
|
||||
public boolean isColorized() {
|
||||
if (isColorizedMedia()) {
|
||||
return true;
|
||||
}
|
||||
return extras.getBoolean(EXTRA_COLORIZED) && isForegroundService();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if this notification is colorized and it is a media notification
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
public boolean isColorizedMedia() {
|
||||
Class<? extends Style> style = getNotificationStyle();
|
||||
if (MediaStyle.class.equals(style)) {
|
||||
Boolean colorized = (Boolean) extras.get(EXTRA_COLORIZED);
|
||||
@@ -4910,7 +4991,7 @@ public class Notification implements Parcelable
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return extras.getBoolean(EXTRA_COLORIZED) && isForegroundService();
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean hasLargeIcon() {
|
||||
|
||||
@@ -257,7 +257,7 @@ public class NotificationColorUtil {
|
||||
* @return a color with the same hue as {@param color}, potentially darkened to meet the
|
||||
* contrast ratio.
|
||||
*/
|
||||
private static int findContrastColor(int color, int other, boolean findFg, double minRatio) {
|
||||
public static int findContrastColor(int color, int other, boolean findFg, double minRatio) {
|
||||
int fg = findFg ? color : other;
|
||||
int bg = findFg ? other : color;
|
||||
if (ColorUtilsFromCompat.calculateContrast(fg, bg) >= minRatio) {
|
||||
@@ -402,16 +402,17 @@ public class NotificationColorUtil {
|
||||
}
|
||||
|
||||
/**
|
||||
* Lighten a color by a specified value
|
||||
* Change a color by a specified value
|
||||
* @param baseColor the base color to lighten
|
||||
* @param amount the amount to lighten the color from 0 to 100. This corresponds to the L
|
||||
* increase in the LAB color space.
|
||||
* @return the lightened color
|
||||
* increase in the LAB color space. A negative value will darken the color and
|
||||
* a positive will lighten it.
|
||||
* @return the changed color
|
||||
*/
|
||||
public static int lightenColor(int baseColor, int amount) {
|
||||
public static int changeColorLightness(int baseColor, int amount) {
|
||||
final double[] result = ColorUtilsFromCompat.getTempDouble3Array();
|
||||
ColorUtilsFromCompat.colorToLAB(baseColor, result);
|
||||
result[0] = Math.min(100, result[0] + amount);
|
||||
result[0] = Math.max(Math.min(100, result[0] + amount), 0);
|
||||
return ColorUtilsFromCompat.LABToColor(result[0], result[1], result[2]);
|
||||
}
|
||||
|
||||
@@ -491,6 +492,15 @@ public class NotificationColorUtil {
|
||||
return useDark;
|
||||
}
|
||||
|
||||
public static double calculateLuminance(int backgroundColor) {
|
||||
return ColorUtilsFromCompat.calculateLuminance(backgroundColor);
|
||||
}
|
||||
|
||||
|
||||
public static double calculateContrast(int foregroundColor, int backgroundColor) {
|
||||
return ColorUtilsFromCompat.calculateContrast(foregroundColor, backgroundColor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Framework copy of functions needed from android.support.v4.graphics.ColorUtils.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
/*
|
||||
* Copyright (C) 2017 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.systemui.statusbar.notification;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.support.v4.graphics.ColorUtils;
|
||||
import android.support.v7.graphics.Palette;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A class the processes media notifications and extracts the right text and background colors.
|
||||
*/
|
||||
public class MediaNotificationProcessor {
|
||||
|
||||
/**
|
||||
* The fraction below which we select the vibrant instead of the light/dark vibrant color
|
||||
*/
|
||||
private static final float POPULATION_FRACTION_FOR_MORE_VIBRANT = 0.75f;
|
||||
private static final float POPULATION_FRACTION_FOR_WHITE_OR_BLACK = 2.5f;
|
||||
private static final float BLACK_MAX_LIGHTNESS = 0.08f;
|
||||
private static final float WHITE_MIN_LIGHTNESS = 0.92f;
|
||||
private static final int RESIZE_BITMAP_AREA = 150 * 150;
|
||||
private float[] mFilteredBackgroundHsl = null;
|
||||
private Palette.Filter mBlackWhiteFilter = (rgb, hsl) -> !isWhiteOrBlack(hsl);
|
||||
|
||||
/**
|
||||
* The context of the notification. This is the app context of the package posting the
|
||||
* notification.
|
||||
*/
|
||||
private final Context mContext;
|
||||
|
||||
public MediaNotificationProcessor(Context context) {
|
||||
mContext = context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a builder of a media notification and calculates the appropriate colors that should
|
||||
* be used.
|
||||
*
|
||||
* @param notification the notification that is being processed
|
||||
* @param builder the recovered builder for the notification. this will be modified
|
||||
*/
|
||||
public void processNotification(Notification notification, Notification.Builder builder) {
|
||||
Icon largeIcon = notification.getLargeIcon();
|
||||
Bitmap bitmap = null;
|
||||
if (largeIcon != null) {
|
||||
Drawable drawable = largeIcon.loadDrawable(mContext);
|
||||
int width = drawable.getIntrinsicWidth();
|
||||
int height = drawable.getIntrinsicHeight();
|
||||
int area = width * height;
|
||||
if (area > RESIZE_BITMAP_AREA) {
|
||||
double factor = Math.sqrt((float) RESIZE_BITMAP_AREA / area);
|
||||
width = (int) (factor * width);
|
||||
height = (int) (factor * height);
|
||||
}
|
||||
bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
||||
Canvas canvas = new Canvas(bitmap);
|
||||
drawable.setBounds(0, 0, width, height);
|
||||
drawable.draw(canvas);
|
||||
}
|
||||
if (bitmap != null) {
|
||||
// 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 palette = paletteBuilder.generate();
|
||||
int backgroundColor = findBackgroundColorAndFilter(palette);
|
||||
// we want the full region again
|
||||
paletteBuilder.setRegion(0, 0, bitmap.getWidth(), bitmap.getHeight());
|
||||
if (mFilteredBackgroundHsl != null) {
|
||||
paletteBuilder.addFilter((rgb, hsl) -> {
|
||||
// at least 10 degrees hue difference
|
||||
float diff = Math.abs(hsl[0] - mFilteredBackgroundHsl[0]);
|
||||
return diff > 10 && diff < 350;
|
||||
});
|
||||
}
|
||||
paletteBuilder.addFilter(mBlackWhiteFilter);
|
||||
palette = paletteBuilder.generate();
|
||||
int foregroundColor;
|
||||
if (ColorUtils.calculateLuminance(backgroundColor) > 0.5) {
|
||||
Palette.Swatch first = palette.getDarkVibrantSwatch();
|
||||
Palette.Swatch second = palette.getVibrantSwatch();
|
||||
if (first != null && second != null) {
|
||||
int firstPopulation = first.getPopulation();
|
||||
int secondPopulation = second.getPopulation();
|
||||
if (firstPopulation / secondPopulation
|
||||
< POPULATION_FRACTION_FOR_MORE_VIBRANT) {
|
||||
foregroundColor = second.getRgb();
|
||||
} else {
|
||||
foregroundColor = first.getRgb();
|
||||
}
|
||||
} else if (first != null) {
|
||||
foregroundColor = first.getRgb();
|
||||
} else if (second != null) {
|
||||
foregroundColor = second.getRgb();
|
||||
} else {
|
||||
first = palette.getMutedSwatch();
|
||||
second = palette.getDarkMutedSwatch();
|
||||
if (first != null && second != null) {
|
||||
float firstSaturation = first.getHsl()[1];
|
||||
float secondSaturation = second.getHsl()[1];
|
||||
if (firstSaturation > secondSaturation) {
|
||||
foregroundColor = first.getRgb();
|
||||
} else {
|
||||
foregroundColor = second.getRgb();
|
||||
}
|
||||
} else if (first != null) {
|
||||
foregroundColor = first.getRgb();
|
||||
} else if (second != null) {
|
||||
foregroundColor = second.getRgb();
|
||||
} else {
|
||||
foregroundColor = Color.BLACK;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Palette.Swatch first = palette.getLightVibrantSwatch();
|
||||
Palette.Swatch second = palette.getVibrantSwatch();
|
||||
if (first != null && second != null) {
|
||||
int firstPopulation = first.getPopulation();
|
||||
int secondPopulation = second.getPopulation();
|
||||
if (firstPopulation / secondPopulation
|
||||
< POPULATION_FRACTION_FOR_MORE_VIBRANT) {
|
||||
foregroundColor = second.getRgb();
|
||||
} else {
|
||||
foregroundColor = first.getRgb();
|
||||
}
|
||||
} else if (first != null) {
|
||||
foregroundColor = first.getRgb();
|
||||
} else if (second != null) {
|
||||
foregroundColor = second.getRgb();
|
||||
} else {
|
||||
first = palette.getMutedSwatch();
|
||||
second = palette.getLightMutedSwatch();
|
||||
if (first != null && second != null) {
|
||||
float firstSaturation = first.getHsl()[1];
|
||||
float secondSaturation = second.getHsl()[1];
|
||||
if (firstSaturation > secondSaturation) {
|
||||
foregroundColor = first.getRgb();
|
||||
} else {
|
||||
foregroundColor = second.getRgb();
|
||||
}
|
||||
} else if (first != null) {
|
||||
foregroundColor = first.getRgb();
|
||||
} else if (second != null) {
|
||||
foregroundColor = second.getRgb();
|
||||
} else {
|
||||
foregroundColor = Color.WHITE;
|
||||
}
|
||||
}
|
||||
}
|
||||
builder.setColorPalette(backgroundColor, foregroundColor);
|
||||
}
|
||||
}
|
||||
|
||||
private int findBackgroundColorAndFilter(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;
|
||||
}
|
||||
|
||||
if (!isWhiteOrBlack(dominantSwatch.getHsl())) {
|
||||
mFilteredBackgroundHsl = dominantSwatch.getHsl();
|
||||
return dominantSwatch.getRgb();
|
||||
}
|
||||
// Oh well, we selected black or white. Lets look at the second color!
|
||||
List<Palette.Swatch> swatches = palette.getSwatches();
|
||||
float highestNonWhitePopulation = -1;
|
||||
Palette.Swatch second = null;
|
||||
for (Palette.Swatch swatch: swatches) {
|
||||
if (swatch != dominantSwatch
|
||||
&& swatch.getPopulation() > highestNonWhitePopulation
|
||||
&& !isWhiteOrBlack(swatch.getHsl())) {
|
||||
second = swatch;
|
||||
highestNonWhitePopulation = swatch.getPopulation();
|
||||
}
|
||||
}
|
||||
if (second == null) {
|
||||
// We're not filtering on white or black
|
||||
mFilteredBackgroundHsl = null;
|
||||
return dominantSwatch.getRgb();
|
||||
}
|
||||
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();
|
||||
} else {
|
||||
mFilteredBackgroundHsl = second.getHsl();
|
||||
return second.getRgb();
|
||||
}
|
||||
}
|
||||
|
||||
private 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) {
|
||||
return hslColor[2] <= BLACK_MAX_LIGHTNESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if the color represents a color which is close to white.
|
||||
*/
|
||||
private boolean isWhite(float[] hslColor) {
|
||||
return hslColor[2] >= WHITE_MIN_LIGHTNESS;
|
||||
}
|
||||
}
|
||||
@@ -309,6 +309,12 @@ public class NotificationInflater {
|
||||
= Notification.Builder.recoverBuilder(mContext,
|
||||
mSbn.getNotification());
|
||||
mPackageContext = mSbn.getPackageContext(mContext);
|
||||
Notification notification = mSbn.getNotification();
|
||||
if (notification.isColorizedMedia()) {
|
||||
MediaNotificationProcessor processor = new MediaNotificationProcessor(
|
||||
mPackageContext);
|
||||
processor.processNotification(notification, recoveredBuilder);
|
||||
}
|
||||
return recoveredBuilder;
|
||||
} catch (Exception e) {
|
||||
mError = e;
|
||||
|
||||
@@ -42,6 +42,7 @@ LOCAL_STATIC_ANDROID_LIBRARIES := \
|
||||
android-support-v7-preference \
|
||||
android-support-v7-appcompat \
|
||||
android-support-v7-mediarouter \
|
||||
android-support-v7-palette \
|
||||
android-support-v14-preference \
|
||||
android-support-v17-leanback
|
||||
|
||||
|
||||
Reference in New Issue
Block a user