1/*
2 * Copyright (C) 2017 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
17package com.android.launcher3.graphics;
18
19import android.app.Notification;
20import android.content.Context;
21import android.content.res.Resources;
22import android.graphics.Color;
23import android.graphics.ColorMatrix;
24import android.graphics.ColorMatrixColorFilter;
25import android.support.annotation.NonNull;
26import android.support.annotation.Nullable;
27import android.support.v4.graphics.ColorUtils;
28import android.util.Log;
29
30import com.android.launcher3.R;
31import com.android.launcher3.util.Themes;
32
33/**
34 * Contains colors based on the dominant color of an icon.
35 */
36public class IconPalette {
37
38    private static final boolean DEBUG = false;
39    private static final String TAG = "IconPalette";
40
41    private static final float MIN_PRELOAD_COLOR_SATURATION = 0.2f;
42    private static final float MIN_PRELOAD_COLOR_LIGHTNESS = 0.6f;
43
44    private static IconPalette sBadgePalette;
45    private static IconPalette sFolderBadgePalette;
46
47    public final int dominantColor;
48    public final int backgroundColor;
49    public final ColorMatrixColorFilter backgroundColorMatrixFilter;
50    public final ColorMatrixColorFilter saturatedBackgroundColorMatrixFilter;
51    public final int textColor;
52    public final int secondaryColor;
53
54    private IconPalette(int color, boolean desaturateBackground) {
55        dominantColor = color;
56        backgroundColor = desaturateBackground ? getMutedColor(dominantColor, 0.87f) : dominantColor;
57        ColorMatrix backgroundColorMatrix = new ColorMatrix();
58        Themes.setColorScaleOnMatrix(backgroundColor, backgroundColorMatrix);
59        backgroundColorMatrixFilter = new ColorMatrixColorFilter(backgroundColorMatrix);
60        if (!desaturateBackground) {
61            saturatedBackgroundColorMatrixFilter = backgroundColorMatrixFilter;
62        } else {
63            // Get slightly more saturated background color.
64            Themes.setColorScaleOnMatrix(getMutedColor(dominantColor, 0.54f), backgroundColorMatrix);
65            saturatedBackgroundColorMatrixFilter = new ColorMatrixColorFilter(backgroundColorMatrix);
66        }
67        textColor = getTextColorForBackground(backgroundColor);
68        secondaryColor = getLowContrastColor(backgroundColor);
69    }
70
71    /**
72     * Returns a color suitable for the progress bar color of preload icon.
73     */
74    public int getPreloadProgressColor(Context context) {
75        int result = dominantColor;
76
77        // Make sure that the dominant color has enough saturation to be visible properly.
78        float[] hsv = new float[3];
79        Color.colorToHSV(result, hsv);
80        if (hsv[1] < MIN_PRELOAD_COLOR_SATURATION) {
81            result = Themes.getColorAccent(context);
82        } else {
83            hsv[2] = Math.max(MIN_PRELOAD_COLOR_LIGHTNESS, hsv[2]);
84            result = Color.HSVToColor(hsv);
85        }
86        return result;
87    }
88
89    public static IconPalette fromDominantColor(int dominantColor, boolean desaturateBackground) {
90        return new IconPalette(dominantColor, desaturateBackground);
91    }
92
93    /**
94     * Returns an IconPalette based on the badge_color in colors.xml.
95     * If that color is Color.TRANSPARENT, then returns null instead.
96     */
97    public static @Nullable IconPalette getBadgePalette(Resources resources) {
98        int badgeColor = resources.getColor(R.color.badge_color);
99        if (badgeColor == Color.TRANSPARENT) {
100            // Colors will be extracted per app icon, so a static palette won't work.
101            return null;
102        }
103        if (sBadgePalette == null) {
104            sBadgePalette = fromDominantColor(badgeColor, false);
105        }
106        return sBadgePalette;
107    }
108
109    /**
110     * Returns an IconPalette based on the folder_badge_color in colors.xml.
111     */
112    public static @NonNull IconPalette getFolderBadgePalette(Resources resources) {
113        if (sFolderBadgePalette == null) {
114            int badgeColor = resources.getColor(R.color.folder_badge_color);
115            sFolderBadgePalette = fromDominantColor(badgeColor, false);
116        }
117        return sFolderBadgePalette;
118    }
119
120    /**
121     * Resolves a color such that it has enough contrast to be used as the
122     * color of an icon or text on the given background color.
123     *
124     * @return a color of the same hue with enough contrast against the background.
125     *
126     * This was copied from com.android.internal.util.NotificationColorUtil.
127     */
128    public static int resolveContrastColor(Context context, int color, int background) {
129        final int resolvedColor = resolveColor(context, color);
130
131        int contrastingColor = ensureTextContrast(resolvedColor, background);
132
133        if (contrastingColor != resolvedColor) {
134            if (DEBUG){
135                Log.w(TAG, String.format(
136                        "Enhanced contrast of notification for %s " +
137                                "%s (over background) by changing #%s to %s",
138                        context.getPackageName(),
139                        contrastChange(resolvedColor, contrastingColor, background),
140                        Integer.toHexString(resolvedColor), Integer.toHexString(contrastingColor)));
141            }
142        }
143        return contrastingColor;
144    }
145
146    /**
147     * Resolves {@param color} to an actual color if it is {@link Notification#COLOR_DEFAULT}
148     *
149     * This was copied from com.android.internal.util.NotificationColorUtil.
150     */
151    private static int resolveColor(Context context, int color) {
152        if (color == Notification.COLOR_DEFAULT) {
153            return context.getColor(R.color.notification_icon_default_color);
154        }
155        return color;
156    }
157
158    /** For debugging. This was copied from com.android.internal.util.NotificationColorUtil. */
159    private static String contrastChange(int colorOld, int colorNew, int bg) {
160        return String.format("from %.2f:1 to %.2f:1",
161                ColorUtils.calculateContrast(colorOld, bg),
162                ColorUtils.calculateContrast(colorNew, bg));
163    }
164
165    /**
166     * Finds a text color with sufficient contrast over bg that has the same hue as the original
167     * color.
168     *
169     * This was copied from com.android.internal.util.NotificationColorUtil.
170     */
171    private static int ensureTextContrast(int color, int bg) {
172        return findContrastColor(color, bg, 4.5);
173    }
174    /**
175     * Finds a suitable color such that there's enough contrast.
176     *
177     * @param fg the color to start searching from.
178     * @param bg the color to ensure contrast against.
179     * @param minRatio the minimum contrast ratio required.
180     * @return a color with the same hue as {@param color}, potentially darkened to meet the
181     *          contrast ratio.
182     *
183     * This was copied from com.android.internal.util.NotificationColorUtil.
184     */
185    private static int findContrastColor(int fg, int bg, double minRatio) {
186        if (ColorUtils.calculateContrast(fg, bg) >= minRatio) {
187            return fg;
188        }
189
190        double[] lab = new double[3];
191        ColorUtils.colorToLAB(bg, lab);
192        double bgL = lab[0];
193        ColorUtils.colorToLAB(fg, lab);
194        double fgL = lab[0];
195        boolean isBgDark = bgL < 50;
196
197        double low = isBgDark ? fgL : 0, high = isBgDark ? 100 : fgL;
198        final double a = lab[1], b = lab[2];
199        for (int i = 0; i < 15 && high - low > 0.00001; i++) {
200            final double l = (low + high) / 2;
201            fg = ColorUtils.LABToColor(l, a, b);
202            if (ColorUtils.calculateContrast(fg, bg) > minRatio) {
203                if (isBgDark) high = l; else low = l;
204            } else {
205                if (isBgDark) low = l; else high = l;
206            }
207        }
208        return ColorUtils.LABToColor(low, a, b);
209    }
210
211    private static int getMutedColor(int color, float whiteScrimAlpha) {
212        int whiteScrim = ColorUtils.setAlphaComponent(Color.WHITE, (int) (255 * whiteScrimAlpha));
213        return ColorUtils.compositeColors(whiteScrim, color);
214    }
215
216    private static int getTextColorForBackground(int backgroundColor) {
217        return getLighterOrDarkerVersionOfColor(backgroundColor, 4.5f);
218    }
219
220    private static int getLowContrastColor(int color) {
221        return getLighterOrDarkerVersionOfColor(color, 1.5f);
222    }
223
224    private static int getLighterOrDarkerVersionOfColor(int color, float contrastRatio) {
225        int whiteMinAlpha = ColorUtils.calculateMinimumAlpha(Color.WHITE, color, contrastRatio);
226        int blackMinAlpha = ColorUtils.calculateMinimumAlpha(Color.BLACK, color, contrastRatio);
227        int translucentWhiteOrBlack;
228        if (whiteMinAlpha >= 0) {
229            translucentWhiteOrBlack = ColorUtils.setAlphaComponent(Color.WHITE, whiteMinAlpha);
230        } else if (blackMinAlpha >= 0) {
231            translucentWhiteOrBlack = ColorUtils.setAlphaComponent(Color.BLACK, blackMinAlpha);
232        } else {
233            translucentWhiteOrBlack = Color.WHITE;
234        }
235        return ColorUtils.compositeColors(translucentWhiteOrBlack, color);
236    }
237}
238