Home | History | Annotate | Download | only in util
      1 /*
      2  * Copyright (C) 2014 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License
     15  */
     16 
     17 package com.android.internal.util;
     18 
     19 import android.annotation.ColorInt;
     20 import android.annotation.FloatRange;
     21 import android.annotation.IntRange;
     22 import android.annotation.NonNull;
     23 import android.app.Notification;
     24 import android.content.Context;
     25 import android.content.res.ColorStateList;
     26 import android.content.res.Resources;
     27 import android.graphics.Bitmap;
     28 import android.graphics.Color;
     29 import android.graphics.drawable.AnimationDrawable;
     30 import android.graphics.drawable.BitmapDrawable;
     31 import android.graphics.drawable.Drawable;
     32 import android.graphics.drawable.Icon;
     33 import android.graphics.drawable.VectorDrawable;
     34 import android.text.SpannableStringBuilder;
     35 import android.text.Spanned;
     36 import android.text.style.BackgroundColorSpan;
     37 import android.text.style.CharacterStyle;
     38 import android.text.style.ForegroundColorSpan;
     39 import android.text.style.TextAppearanceSpan;
     40 import android.util.Log;
     41 import android.util.Pair;
     42 
     43 import java.util.Arrays;
     44 import java.util.WeakHashMap;
     45 
     46 /**
     47  * Helper class to process legacy (Holo) notifications to make them look like material notifications.
     48  *
     49  * @hide
     50  */
     51 public class NotificationColorUtil {
     52 
     53     private static final String TAG = "NotificationColorUtil";
     54     private static final boolean DEBUG = false;
     55 
     56     private static final Object sLock = new Object();
     57     private static NotificationColorUtil sInstance;
     58 
     59     private final ImageUtils mImageUtils = new ImageUtils();
     60     private final WeakHashMap<Bitmap, Pair<Boolean, Integer>> mGrayscaleBitmapCache =
     61             new WeakHashMap<Bitmap, Pair<Boolean, Integer>>();
     62 
     63     private final int mGrayscaleIconMaxSize; // @dimen/notification_large_icon_width (64dp)
     64 
     65     public static NotificationColorUtil getInstance(Context context) {
     66         synchronized (sLock) {
     67             if (sInstance == null) {
     68                 sInstance = new NotificationColorUtil(context);
     69             }
     70             return sInstance;
     71         }
     72     }
     73 
     74     private NotificationColorUtil(Context context) {
     75         mGrayscaleIconMaxSize = context.getResources().getDimensionPixelSize(
     76                 com.android.internal.R.dimen.notification_large_icon_width);
     77     }
     78 
     79     /**
     80      * Checks whether a Bitmap is a small grayscale icon.
     81      * Grayscale here means "very close to a perfect gray"; icon means "no larger than 64dp".
     82      *
     83      * @param bitmap The bitmap to test.
     84      * @return True if the bitmap is grayscale; false if it is color or too large to examine.
     85      */
     86     public boolean isGrayscaleIcon(Bitmap bitmap) {
     87         // quick test: reject large bitmaps
     88         if (bitmap.getWidth() > mGrayscaleIconMaxSize
     89                 || bitmap.getHeight() > mGrayscaleIconMaxSize) {
     90             return false;
     91         }
     92 
     93         synchronized (sLock) {
     94             Pair<Boolean, Integer> cached = mGrayscaleBitmapCache.get(bitmap);
     95             if (cached != null) {
     96                 if (cached.second == bitmap.getGenerationId()) {
     97                     return cached.first;
     98                 }
     99             }
    100         }
    101         boolean result;
    102         int generationId;
    103         synchronized (mImageUtils) {
    104             result = mImageUtils.isGrayscale(bitmap);
    105 
    106             // generationId and the check whether the Bitmap is grayscale can't be read atomically
    107             // here. However, since the thread is in the process of posting the notification, we can
    108             // assume that it doesn't modify the bitmap while we are checking the pixels.
    109             generationId = bitmap.getGenerationId();
    110         }
    111         synchronized (sLock) {
    112             mGrayscaleBitmapCache.put(bitmap, Pair.create(result, generationId));
    113         }
    114         return result;
    115     }
    116 
    117     /**
    118      * Checks whether a Drawable is a small grayscale icon.
    119      * Grayscale here means "very close to a perfect gray"; icon means "no larger than 64dp".
    120      *
    121      * @param d The drawable to test.
    122      * @return True if the bitmap is grayscale; false if it is color or too large to examine.
    123      */
    124     public boolean isGrayscaleIcon(Drawable d) {
    125         if (d == null) {
    126             return false;
    127         } else if (d instanceof BitmapDrawable) {
    128             BitmapDrawable bd = (BitmapDrawable) d;
    129             return bd.getBitmap() != null && isGrayscaleIcon(bd.getBitmap());
    130         } else if (d instanceof AnimationDrawable) {
    131             AnimationDrawable ad = (AnimationDrawable) d;
    132             int count = ad.getNumberOfFrames();
    133             return count > 0 && isGrayscaleIcon(ad.getFrame(0));
    134         } else if (d instanceof VectorDrawable) {
    135             // We just assume you're doing the right thing if using vectors
    136             return true;
    137         } else {
    138             return false;
    139         }
    140     }
    141 
    142     public boolean isGrayscaleIcon(Context context, Icon icon) {
    143         if (icon == null) {
    144             return false;
    145         }
    146         switch (icon.getType()) {
    147             case Icon.TYPE_BITMAP:
    148                 return isGrayscaleIcon(icon.getBitmap());
    149             case Icon.TYPE_RESOURCE:
    150                 return isGrayscaleIcon(context, icon.getResId());
    151             default:
    152                 return false;
    153         }
    154     }
    155 
    156     /**
    157      * Checks whether a drawable with a resoure id is a small grayscale icon.
    158      * Grayscale here means "very close to a perfect gray"; icon means "no larger than 64dp".
    159      *
    160      * @param context The context to load the drawable from.
    161      * @return True if the bitmap is grayscale; false if it is color or too large to examine.
    162      */
    163     public boolean isGrayscaleIcon(Context context, int drawableResId) {
    164         if (drawableResId != 0) {
    165             try {
    166                 return isGrayscaleIcon(context.getDrawable(drawableResId));
    167             } catch (Resources.NotFoundException ex) {
    168                 Log.e(TAG, "Drawable not found: " + drawableResId);
    169                 return false;
    170             }
    171         } else {
    172             return false;
    173         }
    174     }
    175 
    176     /**
    177      * Inverts all the grayscale colors set by {@link android.text.style.TextAppearanceSpan}s on
    178      * the text.
    179      *
    180      * @param charSequence The text to process.
    181      * @return The color inverted text.
    182      */
    183     public CharSequence invertCharSequenceColors(CharSequence charSequence) {
    184         if (charSequence instanceof Spanned) {
    185             Spanned ss = (Spanned) charSequence;
    186             Object[] spans = ss.getSpans(0, ss.length(), Object.class);
    187             SpannableStringBuilder builder = new SpannableStringBuilder(ss.toString());
    188             for (Object span : spans) {
    189                 Object resultSpan = span;
    190                 if (resultSpan instanceof CharacterStyle) {
    191                     resultSpan = ((CharacterStyle) span).getUnderlying();
    192                 }
    193                 if (resultSpan instanceof TextAppearanceSpan) {
    194                     TextAppearanceSpan processedSpan = processTextAppearanceSpan(
    195                             (TextAppearanceSpan) span);
    196                     if (processedSpan != resultSpan) {
    197                         resultSpan = processedSpan;
    198                     } else {
    199                         // we need to still take the orgininal for wrapped spans
    200                         resultSpan = span;
    201                     }
    202                 } else if (resultSpan instanceof ForegroundColorSpan) {
    203                     ForegroundColorSpan originalSpan = (ForegroundColorSpan) resultSpan;
    204                     int foregroundColor = originalSpan.getForegroundColor();
    205                     resultSpan = new ForegroundColorSpan(processColor(foregroundColor));
    206                 } else {
    207                     resultSpan = span;
    208                 }
    209                 builder.setSpan(resultSpan, ss.getSpanStart(span), ss.getSpanEnd(span),
    210                         ss.getSpanFlags(span));
    211             }
    212             return builder;
    213         }
    214         return charSequence;
    215     }
    216 
    217     private TextAppearanceSpan processTextAppearanceSpan(TextAppearanceSpan span) {
    218         ColorStateList colorStateList = span.getTextColor();
    219         if (colorStateList != null) {
    220             int[] colors = colorStateList.getColors();
    221             boolean changed = false;
    222             for (int i = 0; i < colors.length; i++) {
    223                 if (ImageUtils.isGrayscale(colors[i])) {
    224 
    225                     // Allocate a new array so we don't change the colors in the old color state
    226                     // list.
    227                     if (!changed) {
    228                         colors = Arrays.copyOf(colors, colors.length);
    229                     }
    230                     colors[i] = processColor(colors[i]);
    231                     changed = true;
    232                 }
    233             }
    234             if (changed) {
    235                 return new TextAppearanceSpan(
    236                         span.getFamily(), span.getTextStyle(), span.getTextSize(),
    237                         new ColorStateList(colorStateList.getStates(), colors),
    238                         span.getLinkTextColor());
    239             }
    240         }
    241         return span;
    242     }
    243 
    244     /**
    245      * Clears all color spans of a text
    246      * @param charSequence the input text
    247      * @return the same text but without color spans
    248      */
    249     public static CharSequence clearColorSpans(CharSequence charSequence) {
    250         if (charSequence instanceof Spanned) {
    251             Spanned ss = (Spanned) charSequence;
    252             Object[] spans = ss.getSpans(0, ss.length(), Object.class);
    253             SpannableStringBuilder builder = new SpannableStringBuilder(ss.toString());
    254             for (Object span : spans) {
    255                 Object resultSpan = span;
    256                 if (resultSpan instanceof CharacterStyle) {
    257                     resultSpan = ((CharacterStyle) span).getUnderlying();
    258                 }
    259                 if (resultSpan instanceof TextAppearanceSpan) {
    260                     TextAppearanceSpan originalSpan = (TextAppearanceSpan) resultSpan;
    261                     if (originalSpan.getTextColor() != null) {
    262                         resultSpan = new TextAppearanceSpan(
    263                                 originalSpan.getFamily(),
    264                                 originalSpan.getTextStyle(),
    265                                 originalSpan.getTextSize(),
    266                                 null,
    267                                 originalSpan.getLinkTextColor());
    268                     }
    269                 } else if (resultSpan instanceof ForegroundColorSpan
    270                         || (resultSpan instanceof BackgroundColorSpan)) {
    271                     continue;
    272                 } else {
    273                     resultSpan = span;
    274                 }
    275                 builder.setSpan(resultSpan, ss.getSpanStart(span), ss.getSpanEnd(span),
    276                         ss.getSpanFlags(span));
    277             }
    278             return builder;
    279         }
    280         return charSequence;
    281     }
    282 
    283     private int processColor(int color) {
    284         return Color.argb(Color.alpha(color),
    285                 255 - Color.red(color),
    286                 255 - Color.green(color),
    287                 255 - Color.blue(color));
    288     }
    289 
    290     /**
    291      * Finds a suitable color such that there's enough contrast.
    292      *
    293      * @param color the color to start searching from.
    294      * @param other the color to ensure contrast against. Assumed to be lighter than {@param color}
    295      * @param findFg if true, we assume {@param color} is a foreground, otherwise a background.
    296      * @param minRatio the minimum contrast ratio required.
    297      * @return a color with the same hue as {@param color}, potentially darkened to meet the
    298      *          contrast ratio.
    299      */
    300     public static int findContrastColor(int color, int other, boolean findFg, double minRatio) {
    301         int fg = findFg ? color : other;
    302         int bg = findFg ? other : color;
    303         if (ColorUtilsFromCompat.calculateContrast(fg, bg) >= minRatio) {
    304             return color;
    305         }
    306 
    307         double[] lab = new double[3];
    308         ColorUtilsFromCompat.colorToLAB(findFg ? fg : bg, lab);
    309 
    310         double low = 0, high = lab[0];
    311         final double a = lab[1], b = lab[2];
    312         for (int i = 0; i < 15 && high - low > 0.00001; i++) {
    313             final double l = (low + high) / 2;
    314             if (findFg) {
    315                 fg = ColorUtilsFromCompat.LABToColor(l, a, b);
    316             } else {
    317                 bg = ColorUtilsFromCompat.LABToColor(l, a, b);
    318             }
    319             if (ColorUtilsFromCompat.calculateContrast(fg, bg) > minRatio) {
    320                 low = l;
    321             } else {
    322                 high = l;
    323             }
    324         }
    325         return ColorUtilsFromCompat.LABToColor(low, a, b);
    326     }
    327 
    328     /**
    329      * Finds a suitable alpha such that there's enough contrast.
    330      *
    331      * @param color the color to start searching from.
    332      * @param backgroundColor the color to ensure contrast against.
    333      * @param minRatio the minimum contrast ratio required.
    334      * @return the same color as {@param color} with potentially modified alpha to meet contrast
    335      */
    336     public static int findAlphaToMeetContrast(int color, int backgroundColor, double minRatio) {
    337         int fg = color;
    338         int bg = backgroundColor;
    339         if (ColorUtilsFromCompat.calculateContrast(fg, bg) >= minRatio) {
    340             return color;
    341         }
    342         int startAlpha = Color.alpha(color);
    343         int r = Color.red(color);
    344         int g = Color.green(color);
    345         int b = Color.blue(color);
    346 
    347         int low = startAlpha, high = 255;
    348         for (int i = 0; i < 15 && high - low > 0; i++) {
    349             final int alpha = (low + high) / 2;
    350             fg = Color.argb(alpha, r, g, b);
    351             if (ColorUtilsFromCompat.calculateContrast(fg, bg) > minRatio) {
    352                 high = alpha;
    353             } else {
    354                 low = alpha;
    355             }
    356         }
    357         return Color.argb(high, r, g, b);
    358     }
    359 
    360     /**
    361      * Finds a suitable color such that there's enough contrast.
    362      *
    363      * @param color the color to start searching from.
    364      * @param other the color to ensure contrast against. Assumed to be darker than {@param color}
    365      * @param findFg if true, we assume {@param color} is a foreground, otherwise a background.
    366      * @param minRatio the minimum contrast ratio required.
    367      * @return a color with the same hue as {@param color}, potentially darkened to meet the
    368      *          contrast ratio.
    369      */
    370     public static int findContrastColorAgainstDark(int color, int other, boolean findFg,
    371             double minRatio) {
    372         int fg = findFg ? color : other;
    373         int bg = findFg ? other : color;
    374         if (ColorUtilsFromCompat.calculateContrast(fg, bg) >= minRatio) {
    375             return color;
    376         }
    377 
    378         float[] hsl = new float[3];
    379         ColorUtilsFromCompat.colorToHSL(findFg ? fg : bg, hsl);
    380 
    381         float low = hsl[2], high = 1;
    382         for (int i = 0; i < 15 && high - low > 0.00001; i++) {
    383             final float l = (low + high) / 2;
    384             hsl[2] = l;
    385             if (findFg) {
    386                 fg = ColorUtilsFromCompat.HSLToColor(hsl);
    387             } else {
    388                 bg = ColorUtilsFromCompat.HSLToColor(hsl);
    389             }
    390             if (ColorUtilsFromCompat.calculateContrast(fg, bg) > minRatio) {
    391                 high = l;
    392             } else {
    393                 low = l;
    394             }
    395         }
    396         return findFg ? fg : bg;
    397     }
    398 
    399     public static int ensureTextContrastOnBlack(int color) {
    400         return findContrastColorAgainstDark(color, Color.BLACK, true /* fg */, 12);
    401     }
    402 
    403      /**
    404      * Finds a large text color with sufficient contrast over bg that has the same or darker hue as
    405      * the original color, depending on the value of {@code isBgDarker}.
    406      *
    407      * @param isBgDarker {@code true} if {@code bg} is darker than {@code color}.
    408      */
    409     public static int ensureLargeTextContrast(int color, int bg, boolean isBgDarker) {
    410         return isBgDarker
    411                 ? findContrastColorAgainstDark(color, bg, true, 3)
    412                 : findContrastColor(color, bg, true, 3);
    413     }
    414 
    415     /**
    416      * Finds a text color with sufficient contrast over bg that has the same or darker hue as the
    417      * original color, depending on the value of {@code isBgDarker}.
    418      *
    419      * @param isBgDarker {@code true} if {@code bg} is darker than {@code color}.
    420      */
    421     public static int ensureTextContrast(int color, int bg, boolean isBgDarker) {
    422         return ensureContrast(color, bg, isBgDarker, 4.5);
    423     }
    424 
    425     /**
    426      * Finds a color with sufficient contrast over bg that has the same or darker hue as the
    427      * original color, depending on the value of {@code isBgDarker}.
    428      *
    429      * @param color the color to start searching from
    430      * @param bg the color to ensure contrast against
    431      * @param isBgDarker {@code true} if {@code bg} is darker than {@code color}
    432      * @param minRatio the minimum contrast ratio required
    433      */
    434     public static int ensureContrast(int color, int bg, boolean isBgDarker, double minRatio) {
    435         return isBgDarker
    436                 ? findContrastColorAgainstDark(color, bg, true, minRatio)
    437                 : findContrastColor(color, bg, true, minRatio);
    438     }
    439 
    440     /** Finds a background color for a text view with given text color and hint text color, that
    441      * has the same hue as the original color.
    442      */
    443     public static int ensureTextBackgroundColor(int color, int textColor, int hintColor) {
    444         color = findContrastColor(color, hintColor, false, 3.0);
    445         return findContrastColor(color, textColor, false, 4.5);
    446     }
    447 
    448     private static String contrastChange(int colorOld, int colorNew, int bg) {
    449         return String.format("from %.2f:1 to %.2f:1",
    450                 ColorUtilsFromCompat.calculateContrast(colorOld, bg),
    451                 ColorUtilsFromCompat.calculateContrast(colorNew, bg));
    452     }
    453 
    454     /**
    455      * Resolves {@param color} to an actual color if it is {@link Notification#COLOR_DEFAULT}
    456      */
    457     public static int resolveColor(Context context, int color) {
    458         if (color == Notification.COLOR_DEFAULT) {
    459             return context.getColor(com.android.internal.R.color.notification_default_color_light);
    460         }
    461         return color;
    462     }
    463 
    464     /**
    465      * Resolves a Notification's color such that it has enough contrast to be used as the
    466      * color for the Notification's action and header text on a background that is lighter than
    467      * {@code notificationColor}.
    468      *
    469      * @see {@link #resolveContrastColor(Context, int, boolean)}
    470      */
    471     public static int resolveContrastColor(Context context, int notificationColor,
    472             int backgroundColor) {
    473         return NotificationColorUtil.resolveContrastColor(context, notificationColor,
    474                 backgroundColor, false /* isDark */);
    475     }
    476 
    477     /**
    478      * Resolves a Notification's color such that it has enough contrast to be used as the
    479      * color for the Notification's action and header text.
    480      *
    481      * @param notificationColor the color of the notification or {@link Notification#COLOR_DEFAULT}
    482      * @param backgroundColor the background color to ensure the contrast against.
    483      * @param isDark whether or not the {@code notificationColor} will be placed on a background
    484      *               that is darker than the color itself
    485      * @return a color of the same hue with enough contrast against the backgrounds.
    486      */
    487     public static int resolveContrastColor(Context context, int notificationColor,
    488             int backgroundColor, boolean isDark) {
    489         final int resolvedColor = resolveColor(context, notificationColor);
    490 
    491         int color = resolvedColor;
    492         color = NotificationColorUtil.ensureTextContrast(color, backgroundColor, isDark);
    493 
    494         if (color != resolvedColor) {
    495             if (DEBUG){
    496                 Log.w(TAG, String.format(
    497                         "Enhanced contrast of notification for %s"
    498                                 + " and %s (over background) by changing #%s to %s",
    499                         context.getPackageName(),
    500                         NotificationColorUtil.contrastChange(resolvedColor, color, backgroundColor),
    501                         Integer.toHexString(resolvedColor), Integer.toHexString(color)));
    502             }
    503         }
    504         return color;
    505     }
    506 
    507     /**
    508      * Change a color by a specified value
    509      * @param baseColor the base color to lighten
    510      * @param amount the amount to lighten the color from 0 to 100. This corresponds to the L
    511      *               increase in the LAB color space. A negative value will darken the color and
    512      *               a positive will lighten it.
    513      * @return the changed color
    514      */
    515     public static int changeColorLightness(int baseColor, int amount) {
    516         final double[] result = ColorUtilsFromCompat.getTempDouble3Array();
    517         ColorUtilsFromCompat.colorToLAB(baseColor, result);
    518         result[0] = Math.max(Math.min(100, result[0] + amount), 0);
    519         return ColorUtilsFromCompat.LABToColor(result[0], result[1], result[2]);
    520     }
    521 
    522     public static int resolveAmbientColor(Context context, int notificationColor) {
    523         final int resolvedColor = resolveColor(context, notificationColor);
    524 
    525         int color = resolvedColor;
    526         color = NotificationColorUtil.ensureTextContrastOnBlack(color);
    527 
    528         if (color != resolvedColor) {
    529             if (DEBUG){
    530                 Log.w(TAG, String.format(
    531                         "Ambient contrast of notification for %s is %s (over black)"
    532                                 + " by changing #%s to #%s",
    533                         context.getPackageName(),
    534                         NotificationColorUtil.contrastChange(resolvedColor, color, Color.BLACK),
    535                         Integer.toHexString(resolvedColor), Integer.toHexString(color)));
    536             }
    537         }
    538         return color;
    539     }
    540 
    541     public static int resolvePrimaryColor(Context context, int backgroundColor) {
    542         boolean useDark = shouldUseDark(backgroundColor);
    543         if (useDark) {
    544             return context.getColor(
    545                     com.android.internal.R.color.notification_primary_text_color_light);
    546         } else {
    547             return context.getColor(
    548                     com.android.internal.R.color.notification_primary_text_color_dark);
    549         }
    550     }
    551 
    552     public static int resolveSecondaryColor(Context context, int backgroundColor) {
    553         boolean useDark = shouldUseDark(backgroundColor);
    554         if (useDark) {
    555             return context.getColor(
    556                     com.android.internal.R.color.notification_secondary_text_color_light);
    557         } else {
    558             return context.getColor(
    559                     com.android.internal.R.color.notification_secondary_text_color_dark);
    560         }
    561     }
    562 
    563     public static int resolveDefaultColor(Context context, int backgroundColor) {
    564         boolean useDark = shouldUseDark(backgroundColor);
    565         if (useDark) {
    566             return context.getColor(
    567                     com.android.internal.R.color.notification_default_color_light);
    568         } else {
    569             return context.getColor(
    570                     com.android.internal.R.color.notification_default_color_dark);
    571         }
    572     }
    573 
    574     /**
    575      * Get a color that stays in the same tint, but darkens or lightens it by a certain
    576      * amount.
    577      * This also looks at the lightness of the provided color and shifts it appropriately.
    578      *
    579      * @param color the base color to use
    580      * @param amount the amount from 1 to 100 how much to modify the color
    581      * @return the now color that was modified
    582      */
    583     public static int getShiftedColor(int color, int amount) {
    584         final double[] result = ColorUtilsFromCompat.getTempDouble3Array();
    585         ColorUtilsFromCompat.colorToLAB(color, result);
    586         if (result[0] >= 4) {
    587             result[0] = Math.max(0, result[0] - amount);
    588         } else {
    589             result[0] = Math.min(100, result[0] + amount);
    590         }
    591         return ColorUtilsFromCompat.LABToColor(result[0], result[1], result[2]);
    592     }
    593 
    594     private static boolean shouldUseDark(int backgroundColor) {
    595         boolean useDark = backgroundColor == Notification.COLOR_DEFAULT;
    596         if (!useDark) {
    597             useDark = ColorUtilsFromCompat.calculateLuminance(backgroundColor) > 0.5;
    598         }
    599         return useDark;
    600     }
    601 
    602     public static double calculateLuminance(int backgroundColor) {
    603         return ColorUtilsFromCompat.calculateLuminance(backgroundColor);
    604     }
    605 
    606 
    607     public static double calculateContrast(int foregroundColor, int backgroundColor) {
    608         return ColorUtilsFromCompat.calculateContrast(foregroundColor, backgroundColor);
    609     }
    610 
    611     public static boolean satisfiesTextContrast(int backgroundColor, int foregroundColor) {
    612         return NotificationColorUtil.calculateContrast(foregroundColor, backgroundColor) >= 4.5;
    613     }
    614 
    615     /**
    616      * Composite two potentially translucent colors over each other and returns the result.
    617      */
    618     public static int compositeColors(int foreground, int background) {
    619         return ColorUtilsFromCompat.compositeColors(foreground, background);
    620     }
    621 
    622     public static boolean isColorLight(int backgroundColor) {
    623         return calculateLuminance(backgroundColor) > 0.5f;
    624     }
    625 
    626     /**
    627      * Framework copy of functions needed from android.support.v4.graphics.ColorUtils.
    628      */
    629     private static class ColorUtilsFromCompat {
    630         private static final double XYZ_WHITE_REFERENCE_X = 95.047;
    631         private static final double XYZ_WHITE_REFERENCE_Y = 100;
    632         private static final double XYZ_WHITE_REFERENCE_Z = 108.883;
    633         private static final double XYZ_EPSILON = 0.008856;
    634         private static final double XYZ_KAPPA = 903.3;
    635 
    636         private static final int MIN_ALPHA_SEARCH_MAX_ITERATIONS = 10;
    637         private static final int MIN_ALPHA_SEARCH_PRECISION = 1;
    638 
    639         private static final ThreadLocal<double[]> TEMP_ARRAY = new ThreadLocal<>();
    640 
    641         private ColorUtilsFromCompat() {}
    642 
    643         /**
    644          * Composite two potentially translucent colors over each other and returns the result.
    645          */
    646         public static int compositeColors(@ColorInt int foreground, @ColorInt int background) {
    647             int bgAlpha = Color.alpha(background);
    648             int fgAlpha = Color.alpha(foreground);
    649             int a = compositeAlpha(fgAlpha, bgAlpha);
    650 
    651             int r = compositeComponent(Color.red(foreground), fgAlpha,
    652                     Color.red(background), bgAlpha, a);
    653             int g = compositeComponent(Color.green(foreground), fgAlpha,
    654                     Color.green(background), bgAlpha, a);
    655             int b = compositeComponent(Color.blue(foreground), fgAlpha,
    656                     Color.blue(background), bgAlpha, a);
    657 
    658             return Color.argb(a, r, g, b);
    659         }
    660 
    661         private static int compositeAlpha(int foregroundAlpha, int backgroundAlpha) {
    662             return 0xFF - (((0xFF - backgroundAlpha) * (0xFF - foregroundAlpha)) / 0xFF);
    663         }
    664 
    665         private static int compositeComponent(int fgC, int fgA, int bgC, int bgA, int a) {
    666             if (a == 0) return 0;
    667             return ((0xFF * fgC * fgA) + (bgC * bgA * (0xFF - fgA))) / (a * 0xFF);
    668         }
    669 
    670         /**
    671          * Returns the luminance of a color as a float between {@code 0.0} and {@code 1.0}.
    672          * <p>Defined as the Y component in the XYZ representation of {@code color}.</p>
    673          */
    674         @FloatRange(from = 0.0, to = 1.0)
    675         public static double calculateLuminance(@ColorInt int color) {
    676             final double[] result = getTempDouble3Array();
    677             colorToXYZ(color, result);
    678             // Luminance is the Y component
    679             return result[1] / 100;
    680         }
    681 
    682         /**
    683          * Returns the contrast ratio between {@code foreground} and {@code background}.
    684          * {@code background} must be opaque.
    685          * <p>
    686          * Formula defined
    687          * <a href="http://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef">here</a>.
    688          */
    689         public static double calculateContrast(@ColorInt int foreground, @ColorInt int background) {
    690             if (Color.alpha(background) != 255) {
    691                 Log.wtf(TAG, "background can not be translucent: #"
    692                         + Integer.toHexString(background));
    693             }
    694             if (Color.alpha(foreground) < 255) {
    695                 // If the foreground is translucent, composite the foreground over the background
    696                 foreground = compositeColors(foreground, background);
    697             }
    698 
    699             final double luminance1 = calculateLuminance(foreground) + 0.05;
    700             final double luminance2 = calculateLuminance(background) + 0.05;
    701 
    702             // Now return the lighter luminance divided by the darker luminance
    703             return Math.max(luminance1, luminance2) / Math.min(luminance1, luminance2);
    704         }
    705 
    706         /**
    707          * Convert the ARGB color to its CIE Lab representative components.
    708          *
    709          * @param color  the ARGB color to convert. The alpha component is ignored
    710          * @param outLab 3-element array which holds the resulting LAB components
    711          */
    712         public static void colorToLAB(@ColorInt int color, @NonNull double[] outLab) {
    713             RGBToLAB(Color.red(color), Color.green(color), Color.blue(color), outLab);
    714         }
    715 
    716         /**
    717          * Convert RGB components to its CIE Lab representative components.
    718          *
    719          * <ul>
    720          * <li>outLab[0] is L [0 ...100)</li>
    721          * <li>outLab[1] is a [-128...127)</li>
    722          * <li>outLab[2] is b [-128...127)</li>
    723          * </ul>
    724          *
    725          * @param r      red component value [0..255]
    726          * @param g      green component value [0..255]
    727          * @param b      blue component value [0..255]
    728          * @param outLab 3-element array which holds the resulting LAB components
    729          */
    730         public static void RGBToLAB(@IntRange(from = 0x0, to = 0xFF) int r,
    731                 @IntRange(from = 0x0, to = 0xFF) int g, @IntRange(from = 0x0, to = 0xFF) int b,
    732                 @NonNull double[] outLab) {
    733             // First we convert RGB to XYZ
    734             RGBToXYZ(r, g, b, outLab);
    735             // outLab now contains XYZ
    736             XYZToLAB(outLab[0], outLab[1], outLab[2], outLab);
    737             // outLab now contains LAB representation
    738         }
    739 
    740         /**
    741          * Convert the ARGB color to it's CIE XYZ representative components.
    742          *
    743          * <p>The resulting XYZ representation will use the D65 illuminant and the CIE
    744          * 2 Standard Observer (1931).</p>
    745          *
    746          * <ul>
    747          * <li>outXyz[0] is X [0 ...95.047)</li>
    748          * <li>outXyz[1] is Y [0...100)</li>
    749          * <li>outXyz[2] is Z [0...108.883)</li>
    750          * </ul>
    751          *
    752          * @param color  the ARGB color to convert. The alpha component is ignored
    753          * @param outXyz 3-element array which holds the resulting LAB components
    754          */
    755         public static void colorToXYZ(@ColorInt int color, @NonNull double[] outXyz) {
    756             RGBToXYZ(Color.red(color), Color.green(color), Color.blue(color), outXyz);
    757         }
    758 
    759         /**
    760          * Convert RGB components to it's CIE XYZ representative components.
    761          *
    762          * <p>The resulting XYZ representation will use the D65 illuminant and the CIE
    763          * 2 Standard Observer (1931).</p>
    764          *
    765          * <ul>
    766          * <li>outXyz[0] is X [0 ...95.047)</li>
    767          * <li>outXyz[1] is Y [0...100)</li>
    768          * <li>outXyz[2] is Z [0...108.883)</li>
    769          * </ul>
    770          *
    771          * @param r      red component value [0..255]
    772          * @param g      green component value [0..255]
    773          * @param b      blue component value [0..255]
    774          * @param outXyz 3-element array which holds the resulting XYZ components
    775          */
    776         public static void RGBToXYZ(@IntRange(from = 0x0, to = 0xFF) int r,
    777                 @IntRange(from = 0x0, to = 0xFF) int g, @IntRange(from = 0x0, to = 0xFF) int b,
    778                 @NonNull double[] outXyz) {
    779             if (outXyz.length != 3) {
    780                 throw new IllegalArgumentException("outXyz must have a length of 3.");
    781             }
    782 
    783             double sr = r / 255.0;
    784             sr = sr < 0.04045 ? sr / 12.92 : Math.pow((sr + 0.055) / 1.055, 2.4);
    785             double sg = g / 255.0;
    786             sg = sg < 0.04045 ? sg / 12.92 : Math.pow((sg + 0.055) / 1.055, 2.4);
    787             double sb = b / 255.0;
    788             sb = sb < 0.04045 ? sb / 12.92 : Math.pow((sb + 0.055) / 1.055, 2.4);
    789 
    790             outXyz[0] = 100 * (sr * 0.4124 + sg * 0.3576 + sb * 0.1805);
    791             outXyz[1] = 100 * (sr * 0.2126 + sg * 0.7152 + sb * 0.0722);
    792             outXyz[2] = 100 * (sr * 0.0193 + sg * 0.1192 + sb * 0.9505);
    793         }
    794 
    795         /**
    796          * Converts a color from CIE XYZ to CIE Lab representation.
    797          *
    798          * <p>This method expects the XYZ representation to use the D65 illuminant and the CIE
    799          * 2 Standard Observer (1931).</p>
    800          *
    801          * <ul>
    802          * <li>outLab[0] is L [0 ...100)</li>
    803          * <li>outLab[1] is a [-128...127)</li>
    804          * <li>outLab[2] is b [-128...127)</li>
    805          * </ul>
    806          *
    807          * @param x      X component value [0...95.047)
    808          * @param y      Y component value [0...100)
    809          * @param z      Z component value [0...108.883)
    810          * @param outLab 3-element array which holds the resulting Lab components
    811          */
    812         public static void XYZToLAB(@FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_X) double x,
    813                 @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Y) double y,
    814                 @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Z) double z,
    815                 @NonNull double[] outLab) {
    816             if (outLab.length != 3) {
    817                 throw new IllegalArgumentException("outLab must have a length of 3.");
    818             }
    819             x = pivotXyzComponent(x / XYZ_WHITE_REFERENCE_X);
    820             y = pivotXyzComponent(y / XYZ_WHITE_REFERENCE_Y);
    821             z = pivotXyzComponent(z / XYZ_WHITE_REFERENCE_Z);
    822             outLab[0] = Math.max(0, 116 * y - 16);
    823             outLab[1] = 500 * (x - y);
    824             outLab[2] = 200 * (y - z);
    825         }
    826 
    827         /**
    828          * Converts a color from CIE Lab to CIE XYZ representation.
    829          *
    830          * <p>The resulting XYZ representation will use the D65 illuminant and the CIE
    831          * 2 Standard Observer (1931).</p>
    832          *
    833          * <ul>
    834          * <li>outXyz[0] is X [0 ...95.047)</li>
    835          * <li>outXyz[1] is Y [0...100)</li>
    836          * <li>outXyz[2] is Z [0...108.883)</li>
    837          * </ul>
    838          *
    839          * @param l      L component value [0...100)
    840          * @param a      A component value [-128...127)
    841          * @param b      B component value [-128...127)
    842          * @param outXyz 3-element array which holds the resulting XYZ components
    843          */
    844         public static void LABToXYZ(@FloatRange(from = 0f, to = 100) final double l,
    845                 @FloatRange(from = -128, to = 127) final double a,
    846                 @FloatRange(from = -128, to = 127) final double b,
    847                 @NonNull double[] outXyz) {
    848             final double fy = (l + 16) / 116;
    849             final double fx = a / 500 + fy;
    850             final double fz = fy - b / 200;
    851 
    852             double tmp = Math.pow(fx, 3);
    853             final double xr = tmp > XYZ_EPSILON ? tmp : (116 * fx - 16) / XYZ_KAPPA;
    854             final double yr = l > XYZ_KAPPA * XYZ_EPSILON ? Math.pow(fy, 3) : l / XYZ_KAPPA;
    855 
    856             tmp = Math.pow(fz, 3);
    857             final double zr = tmp > XYZ_EPSILON ? tmp : (116 * fz - 16) / XYZ_KAPPA;
    858 
    859             outXyz[0] = xr * XYZ_WHITE_REFERENCE_X;
    860             outXyz[1] = yr * XYZ_WHITE_REFERENCE_Y;
    861             outXyz[2] = zr * XYZ_WHITE_REFERENCE_Z;
    862         }
    863 
    864         /**
    865          * Converts a color from CIE XYZ to its RGB representation.
    866          *
    867          * <p>This method expects the XYZ representation to use the D65 illuminant and the CIE
    868          * 2 Standard Observer (1931).</p>
    869          *
    870          * @param x X component value [0...95.047)
    871          * @param y Y component value [0...100)
    872          * @param z Z component value [0...108.883)
    873          * @return int containing the RGB representation
    874          */
    875         @ColorInt
    876         public static int XYZToColor(@FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_X) double x,
    877                 @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Y) double y,
    878                 @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Z) double z) {
    879             double r = (x * 3.2406 + y * -1.5372 + z * -0.4986) / 100;
    880             double g = (x * -0.9689 + y * 1.8758 + z * 0.0415) / 100;
    881             double b = (x * 0.0557 + y * -0.2040 + z * 1.0570) / 100;
    882 
    883             r = r > 0.0031308 ? 1.055 * Math.pow(r, 1 / 2.4) - 0.055 : 12.92 * r;
    884             g = g > 0.0031308 ? 1.055 * Math.pow(g, 1 / 2.4) - 0.055 : 12.92 * g;
    885             b = b > 0.0031308 ? 1.055 * Math.pow(b, 1 / 2.4) - 0.055 : 12.92 * b;
    886 
    887             return Color.rgb(
    888                     constrain((int) Math.round(r * 255), 0, 255),
    889                     constrain((int) Math.round(g * 255), 0, 255),
    890                     constrain((int) Math.round(b * 255), 0, 255));
    891         }
    892 
    893         /**
    894          * Converts a color from CIE Lab to its RGB representation.
    895          *
    896          * @param l L component value [0...100]
    897          * @param a A component value [-128...127]
    898          * @param b B component value [-128...127]
    899          * @return int containing the RGB representation
    900          */
    901         @ColorInt
    902         public static int LABToColor(@FloatRange(from = 0f, to = 100) final double l,
    903                 @FloatRange(from = -128, to = 127) final double a,
    904                 @FloatRange(from = -128, to = 127) final double b) {
    905             final double[] result = getTempDouble3Array();
    906             LABToXYZ(l, a, b, result);
    907             return XYZToColor(result[0], result[1], result[2]);
    908         }
    909 
    910         private static int constrain(int amount, int low, int high) {
    911             return amount < low ? low : (amount > high ? high : amount);
    912         }
    913 
    914         private static float constrain(float amount, float low, float high) {
    915             return amount < low ? low : (amount > high ? high : amount);
    916         }
    917 
    918         private static double pivotXyzComponent(double component) {
    919             return component > XYZ_EPSILON
    920                     ? Math.pow(component, 1 / 3.0)
    921                     : (XYZ_KAPPA * component + 16) / 116;
    922         }
    923 
    924         public static double[] getTempDouble3Array() {
    925             double[] result = TEMP_ARRAY.get();
    926             if (result == null) {
    927                 result = new double[3];
    928                 TEMP_ARRAY.set(result);
    929             }
    930             return result;
    931         }
    932 
    933         /**
    934          * Convert HSL (hue-saturation-lightness) components to a RGB color.
    935          * <ul>
    936          * <li>hsl[0] is Hue [0 .. 360)</li>
    937          * <li>hsl[1] is Saturation [0...1]</li>
    938          * <li>hsl[2] is Lightness [0...1]</li>
    939          * </ul>
    940          * If hsv values are out of range, they are pinned.
    941          *
    942          * @param hsl 3-element array which holds the input HSL components
    943          * @return the resulting RGB color
    944          */
    945         @ColorInt
    946         public static int HSLToColor(@NonNull float[] hsl) {
    947             final float h = hsl[0];
    948             final float s = hsl[1];
    949             final float l = hsl[2];
    950 
    951             final float c = (1f - Math.abs(2 * l - 1f)) * s;
    952             final float m = l - 0.5f * c;
    953             final float x = c * (1f - Math.abs((h / 60f % 2f) - 1f));
    954 
    955             final int hueSegment = (int) h / 60;
    956 
    957             int r = 0, g = 0, b = 0;
    958 
    959             switch (hueSegment) {
    960                 case 0:
    961                     r = Math.round(255 * (c + m));
    962                     g = Math.round(255 * (x + m));
    963                     b = Math.round(255 * m);
    964                     break;
    965                 case 1:
    966                     r = Math.round(255 * (x + m));
    967                     g = Math.round(255 * (c + m));
    968                     b = Math.round(255 * m);
    969                     break;
    970                 case 2:
    971                     r = Math.round(255 * m);
    972                     g = Math.round(255 * (c + m));
    973                     b = Math.round(255 * (x + m));
    974                     break;
    975                 case 3:
    976                     r = Math.round(255 * m);
    977                     g = Math.round(255 * (x + m));
    978                     b = Math.round(255 * (c + m));
    979                     break;
    980                 case 4:
    981                     r = Math.round(255 * (x + m));
    982                     g = Math.round(255 * m);
    983                     b = Math.round(255 * (c + m));
    984                     break;
    985                 case 5:
    986                 case 6:
    987                     r = Math.round(255 * (c + m));
    988                     g = Math.round(255 * m);
    989                     b = Math.round(255 * (x + m));
    990                     break;
    991             }
    992 
    993             r = constrain(r, 0, 255);
    994             g = constrain(g, 0, 255);
    995             b = constrain(b, 0, 255);
    996 
    997             return Color.rgb(r, g, b);
    998         }
    999 
   1000         /**
   1001          * Convert the ARGB color to its HSL (hue-saturation-lightness) components.
   1002          * <ul>
   1003          * <li>outHsl[0] is Hue [0 .. 360)</li>
   1004          * <li>outHsl[1] is Saturation [0...1]</li>
   1005          * <li>outHsl[2] is Lightness [0...1]</li>
   1006          * </ul>
   1007          *
   1008          * @param color  the ARGB color to convert. The alpha component is ignored
   1009          * @param outHsl 3-element array which holds the resulting HSL components
   1010          */
   1011         public static void colorToHSL(@ColorInt int color, @NonNull float[] outHsl) {
   1012             RGBToHSL(Color.red(color), Color.green(color), Color.blue(color), outHsl);
   1013         }
   1014 
   1015         /**
   1016          * Convert RGB components to HSL (hue-saturation-lightness).
   1017          * <ul>
   1018          * <li>outHsl[0] is Hue [0 .. 360)</li>
   1019          * <li>outHsl[1] is Saturation [0...1]</li>
   1020          * <li>outHsl[2] is Lightness [0...1]</li>
   1021          * </ul>
   1022          *
   1023          * @param r      red component value [0..255]
   1024          * @param g      green component value [0..255]
   1025          * @param b      blue component value [0..255]
   1026          * @param outHsl 3-element array which holds the resulting HSL components
   1027          */
   1028         public static void RGBToHSL(@IntRange(from = 0x0, to = 0xFF) int r,
   1029                 @IntRange(from = 0x0, to = 0xFF) int g, @IntRange(from = 0x0, to = 0xFF) int b,
   1030                 @NonNull float[] outHsl) {
   1031             final float rf = r / 255f;
   1032             final float gf = g / 255f;
   1033             final float bf = b / 255f;
   1034 
   1035             final float max = Math.max(rf, Math.max(gf, bf));
   1036             final float min = Math.min(rf, Math.min(gf, bf));
   1037             final float deltaMaxMin = max - min;
   1038 
   1039             float h, s;
   1040             float l = (max + min) / 2f;
   1041 
   1042             if (max == min) {
   1043                 // Monochromatic
   1044                 h = s = 0f;
   1045             } else {
   1046                 if (max == rf) {
   1047                     h = ((gf - bf) / deltaMaxMin) % 6f;
   1048                 } else if (max == gf) {
   1049                     h = ((bf - rf) / deltaMaxMin) + 2f;
   1050                 } else {
   1051                     h = ((rf - gf) / deltaMaxMin) + 4f;
   1052                 }
   1053 
   1054                 s = deltaMaxMin / (1f - Math.abs(2f * l - 1f));
   1055             }
   1056 
   1057             h = (h * 60f) % 360f;
   1058             if (h < 0) {
   1059                 h += 360f;
   1060             }
   1061 
   1062             outHsl[0] = constrain(h, 0f, 360f);
   1063             outHsl[1] = constrain(s, 0f, 1f);
   1064             outHsl[2] = constrain(l, 0f, 1f);
   1065         }
   1066 
   1067     }
   1068 }
   1069