Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 5fb73f86 authored by Selim Cinek's avatar Selim Cinek
Browse files

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
Merged-In: I9c3c962fa59eb70ef9b2d4893b939be6e1ee1ab0
Change-Id: I9c3c962fa59eb70ef9b2d4893b939be6e1ee1ab0
parent 2630dc7e
Loading
Loading
Loading
Loading
+87 −6
Original line number Diff line number Diff line
@@ -2663,6 +2663,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:
@@ -3819,10 +3821,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);
            }
@@ -4810,7 +4864,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;
            }
@@ -4828,6 +4882,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();
        }
    }

    /**
@@ -4864,6 +4933,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);
@@ -4875,7 +4956,7 @@ public class Notification implements Parcelable
                return true;
            }
        }
        return extras.getBoolean(EXTRA_COLORIZED) && isForegroundService();
        return false;
    }

    private boolean hasLargeIcon() {
+16 −6
Original line number Diff line number Diff line
@@ -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.
     */
+238 −0
Original line number Diff line number Diff line
/*
 * 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;
    }
}
+6 −0
Original line number Diff line number Diff line
@@ -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;
+1 −0
Original line number Diff line number Diff line
@@ -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