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:
|| 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);
}
private int getBackgroundColor() {
if (isColorized()) {
- return mN.color;
+ return mBackgroundColor != COLOR_INVALID ? mBackgroundColor : mN.color;
} else {
return COLOR_DEFAULT;
}
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();
+ }
}
/**
* @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);
return true;
}
}
- return extras.getBoolean(EXTRA_COLORIZED) && isForegroundService();
+ return false;
}
private boolean hasLargeIcon() {
* @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) {
}
/**
- * 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]);
}
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.
*/
--- /dev/null
+/*
+ * 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;
+ }
+}