diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index 9296d4447f59f..b862dac801ac0 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -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 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() { diff --git a/core/java/com/android/internal/util/NotificationColorUtil.java b/core/java/com/android/internal/util/NotificationColorUtil.java index 5cb66e501393e..2c97f8bd59718 100644 --- a/core/java/com/android/internal/util/NotificationColorUtil.java +++ b/core/java/com/android/internal/util/NotificationColorUtil.java @@ -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. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/MediaNotificationProcessor.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/MediaNotificationProcessor.java new file mode 100644 index 0000000000000..ccba664025231 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/MediaNotificationProcessor.java @@ -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 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; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationInflater.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationInflater.java index 3bad5cc7cdee9..77fc5e6973fe4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationInflater.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationInflater.java @@ -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; diff --git a/packages/SystemUI/tests/Android.mk b/packages/SystemUI/tests/Android.mk index 8eedf3145e614..5e8b3f905258f 100644 --- a/packages/SystemUI/tests/Android.mk +++ b/packages/SystemUI/tests/Android.mk @@ -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