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:
Selim Cinek
2017-04-20 16:55:38 -07:00
parent 5ba2254df1
commit fb49ffc96a
5 changed files with 348 additions and 12 deletions

View File

@@ -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() {

View File

@@ -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.
*/

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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