OSDN Git Service

Extracting the notification colors based on the album art
authorSelim Cinek <cinek@google.com>
Thu, 20 Apr 2017 23:55:38 +0000 (16:55 -0700)
committerSelim Cinek <cinek@google.com>
Thu, 27 Apr 2017 19:37:34 +0000 (12:37 -0700)
Media notifications are now extracting the background and
foreground colors from the album art.

Test: manual, play different songs
Bug: 36561228
Change-Id: I9c3c962fa59eb70ef9b2d4893b939be6e1ee1ab0

core/java/android/app/Notification.java
core/java/com/android/internal/util/NotificationColorUtil.java
packages/SystemUI/src/com/android/systemui/statusbar/notification/MediaNotificationProcessor.java [new file with mode: 0644]
packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationInflater.java
packages/SystemUI/tests/Android.mk

index 9296d44..b862dac 100644 (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() {
index 5cb66e5..2c97f8b 100644 (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.
      */
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 (file)
index 0000000..ccba664
--- /dev/null
@@ -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;
+    }
+}
index 3bad5cc..77fc5e6 100644 (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;
index 8eedf31..5e8b3f9 100644 (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