blob: 2778d93a53885a0d88139b79aa58d3ade35b614f [file] [log] [blame]
Jorim Jaggi5c2d8462014-03-21 17:37:00 +01001/*
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
17package com.android.internal.util;
18
Adrian Roos4ff3b122016-02-01 12:26:13 -080019import android.annotation.ColorInt;
20import android.annotation.FloatRange;
21import android.annotation.IntRange;
22import android.annotation.NonNull;
23import android.app.Notification;
Jorim Jaggi5c2d8462014-03-21 17:37:00 +010024import android.content.Context;
25import android.content.res.ColorStateList;
26import android.content.res.Resources;
27import android.graphics.Bitmap;
28import android.graphics.Color;
29import android.graphics.drawable.AnimationDrawable;
30import android.graphics.drawable.BitmapDrawable;
31import android.graphics.drawable.Drawable;
Dan Sandlerd63f9322015-05-06 15:18:49 -040032import android.graphics.drawable.Icon;
Dan Sandler26e81cf2014-05-06 10:01:27 -040033import android.graphics.drawable.VectorDrawable;
Jorim Jaggi5c2d8462014-03-21 17:37:00 +010034import android.text.SpannableStringBuilder;
35import android.text.Spanned;
Selim Cinek7b9605b2017-01-19 17:36:00 -080036import android.text.style.CharacterStyle;
37import android.text.style.ForegroundColorSpan;
Jorim Jaggi5c2d8462014-03-21 17:37:00 +010038import android.text.style.TextAppearanceSpan;
39import android.util.Log;
40import android.util.Pair;
41
42import java.util.Arrays;
43import java.util.WeakHashMap;
44
45/**
Alan Viverette830960c2014-06-06 15:48:55 -070046 * Helper class to process legacy (Holo) notifications to make them look like material notifications.
Jorim Jaggi5c2d8462014-03-21 17:37:00 +010047 *
48 * @hide
49 */
Dan Sandler26e81cf2014-05-06 10:01:27 -040050public class NotificationColorUtil {
Jorim Jaggi5c2d8462014-03-21 17:37:00 +010051
Dan Sandler26e81cf2014-05-06 10:01:27 -040052 private static final String TAG = "NotificationColorUtil";
Adrian Roos4ff3b122016-02-01 12:26:13 -080053 private static final boolean DEBUG = false;
Jorim Jaggi5c2d8462014-03-21 17:37:00 +010054
55 private static final Object sLock = new Object();
Dan Sandler26e81cf2014-05-06 10:01:27 -040056 private static NotificationColorUtil sInstance;
Jorim Jaggi5c2d8462014-03-21 17:37:00 +010057
58 private final ImageUtils mImageUtils = new ImageUtils();
59 private final WeakHashMap<Bitmap, Pair<Boolean, Integer>> mGrayscaleBitmapCache =
60 new WeakHashMap<Bitmap, Pair<Boolean, Integer>>();
61
Dan Sandler05c362d2014-09-03 00:16:27 +020062 private final int mGrayscaleIconMaxSize; // @dimen/notification_large_icon_width (64dp)
63
64 public static NotificationColorUtil getInstance(Context context) {
Jorim Jaggi5c2d8462014-03-21 17:37:00 +010065 synchronized (sLock) {
66 if (sInstance == null) {
Dan Sandler05c362d2014-09-03 00:16:27 +020067 sInstance = new NotificationColorUtil(context);
Jorim Jaggi5c2d8462014-03-21 17:37:00 +010068 }
69 return sInstance;
70 }
71 }
72
Dan Sandler05c362d2014-09-03 00:16:27 +020073 private NotificationColorUtil(Context context) {
74 mGrayscaleIconMaxSize = context.getResources().getDimensionPixelSize(
75 com.android.internal.R.dimen.notification_large_icon_width);
76 }
77
Jorim Jaggi5c2d8462014-03-21 17:37:00 +010078 /**
Dan Sandler05c362d2014-09-03 00:16:27 +020079 * Checks whether a Bitmap is a small grayscale icon.
80 * Grayscale here means "very close to a perfect gray"; icon means "no larger than 64dp".
Jorim Jaggi5c2d8462014-03-21 17:37:00 +010081 *
82 * @param bitmap The bitmap to test.
Dan Sandler05c362d2014-09-03 00:16:27 +020083 * @return True if the bitmap is grayscale; false if it is color or too large to examine.
Jorim Jaggi5c2d8462014-03-21 17:37:00 +010084 */
Dan Sandler05c362d2014-09-03 00:16:27 +020085 public boolean isGrayscaleIcon(Bitmap bitmap) {
86 // quick test: reject large bitmaps
87 if (bitmap.getWidth() > mGrayscaleIconMaxSize
88 || bitmap.getHeight() > mGrayscaleIconMaxSize) {
89 return false;
90 }
91
Jorim Jaggi5c2d8462014-03-21 17:37:00 +010092 synchronized (sLock) {
93 Pair<Boolean, Integer> cached = mGrayscaleBitmapCache.get(bitmap);
94 if (cached != null) {
95 if (cached.second == bitmap.getGenerationId()) {
96 return cached.first;
97 }
98 }
99 }
100 boolean result;
101 int generationId;
102 synchronized (mImageUtils) {
103 result = mImageUtils.isGrayscale(bitmap);
104
105 // generationId and the check whether the Bitmap is grayscale can't be read atomically
106 // here. However, since the thread is in the process of posting the notification, we can
107 // assume that it doesn't modify the bitmap while we are checking the pixels.
108 generationId = bitmap.getGenerationId();
109 }
110 synchronized (sLock) {
111 mGrayscaleBitmapCache.put(bitmap, Pair.create(result, generationId));
112 }
113 return result;
114 }
115
116 /**
Dan Sandler05c362d2014-09-03 00:16:27 +0200117 * Checks whether a Drawable is a small grayscale icon.
118 * Grayscale here means "very close to a perfect gray"; icon means "no larger than 64dp".
Jorim Jaggi5c2d8462014-03-21 17:37:00 +0100119 *
120 * @param d The drawable to test.
Dan Sandler05c362d2014-09-03 00:16:27 +0200121 * @return True if the bitmap is grayscale; false if it is color or too large to examine.
Jorim Jaggi5c2d8462014-03-21 17:37:00 +0100122 */
Dan Sandler05c362d2014-09-03 00:16:27 +0200123 public boolean isGrayscaleIcon(Drawable d) {
Jorim Jaggi5c2d8462014-03-21 17:37:00 +0100124 if (d == null) {
125 return false;
126 } else if (d instanceof BitmapDrawable) {
127 BitmapDrawable bd = (BitmapDrawable) d;
Dan Sandler05c362d2014-09-03 00:16:27 +0200128 return bd.getBitmap() != null && isGrayscaleIcon(bd.getBitmap());
Jorim Jaggi5c2d8462014-03-21 17:37:00 +0100129 } else if (d instanceof AnimationDrawable) {
130 AnimationDrawable ad = (AnimationDrawable) d;
131 int count = ad.getNumberOfFrames();
Dan Sandler05c362d2014-09-03 00:16:27 +0200132 return count > 0 && isGrayscaleIcon(ad.getFrame(0));
Dan Sandler26e81cf2014-05-06 10:01:27 -0400133 } else if (d instanceof VectorDrawable) {
134 // We just assume you're doing the right thing if using vectors
135 return true;
Jorim Jaggi5c2d8462014-03-21 17:37:00 +0100136 } else {
137 return false;
138 }
139 }
140
Dan Sandlerd63f9322015-05-06 15:18:49 -0400141 public boolean isGrayscaleIcon(Context context, Icon icon) {
142 if (icon == null) {
143 return false;
144 }
145 switch (icon.getType()) {
146 case Icon.TYPE_BITMAP:
147 return isGrayscaleIcon(icon.getBitmap());
148 case Icon.TYPE_RESOURCE:
149 return isGrayscaleIcon(context, icon.getResId());
150 default:
151 return false;
152 }
153 }
154
Jorim Jaggi5c2d8462014-03-21 17:37:00 +0100155 /**
Dan Sandler05c362d2014-09-03 00:16:27 +0200156 * Checks whether a drawable with a resoure id is a small grayscale icon.
157 * Grayscale here means "very close to a perfect gray"; icon means "no larger than 64dp".
Jorim Jaggi5c2d8462014-03-21 17:37:00 +0100158 *
159 * @param context The context to load the drawable from.
Dan Sandler05c362d2014-09-03 00:16:27 +0200160 * @return True if the bitmap is grayscale; false if it is color or too large to examine.
Jorim Jaggi5c2d8462014-03-21 17:37:00 +0100161 */
Dan Sandler05c362d2014-09-03 00:16:27 +0200162 public boolean isGrayscaleIcon(Context context, int drawableResId) {
Jorim Jaggi5c2d8462014-03-21 17:37:00 +0100163 if (drawableResId != 0) {
164 try {
Dan Sandler05c362d2014-09-03 00:16:27 +0200165 return isGrayscaleIcon(context.getDrawable(drawableResId));
Jorim Jaggi5c2d8462014-03-21 17:37:00 +0100166 } catch (Resources.NotFoundException ex) {
167 Log.e(TAG, "Drawable not found: " + drawableResId);
168 return false;
169 }
170 } else {
171 return false;
172 }
173 }
174
175 /**
176 * Inverts all the grayscale colors set by {@link android.text.style.TextAppearanceSpan}s on
177 * the text.
178 *
179 * @param charSequence The text to process.
180 * @return The color inverted text.
181 */
182 public CharSequence invertCharSequenceColors(CharSequence charSequence) {
183 if (charSequence instanceof Spanned) {
184 Spanned ss = (Spanned) charSequence;
185 Object[] spans = ss.getSpans(0, ss.length(), Object.class);
186 SpannableStringBuilder builder = new SpannableStringBuilder(ss.toString());
187 for (Object span : spans) {
188 Object resultSpan = span;
Selim Cinek7b9605b2017-01-19 17:36:00 -0800189 if (resultSpan instanceof CharacterStyle) {
190 resultSpan = ((CharacterStyle) span).getUnderlying();
191 }
192 if (resultSpan instanceof TextAppearanceSpan) {
193 TextAppearanceSpan processedSpan = processTextAppearanceSpan(
194 (TextAppearanceSpan) span);
195 if (processedSpan != resultSpan) {
196 resultSpan = processedSpan;
197 } else {
198 // we need to still take the orgininal for wrapped spans
199 resultSpan = span;
200 }
201 } else if (resultSpan instanceof ForegroundColorSpan) {
202 ForegroundColorSpan originalSpan = (ForegroundColorSpan) resultSpan;
203 int foregroundColor = originalSpan.getForegroundColor();
204 resultSpan = new ForegroundColorSpan(processColor(foregroundColor));
205 } else {
206 resultSpan = span;
Jorim Jaggi5c2d8462014-03-21 17:37:00 +0100207 }
208 builder.setSpan(resultSpan, ss.getSpanStart(span), ss.getSpanEnd(span),
209 ss.getSpanFlags(span));
210 }
211 return builder;
212 }
213 return charSequence;
214 }
215
216 private TextAppearanceSpan processTextAppearanceSpan(TextAppearanceSpan span) {
217 ColorStateList colorStateList = span.getTextColor();
218 if (colorStateList != null) {
219 int[] colors = colorStateList.getColors();
220 boolean changed = false;
221 for (int i = 0; i < colors.length; i++) {
222 if (ImageUtils.isGrayscale(colors[i])) {
223
224 // Allocate a new array so we don't change the colors in the old color state
225 // list.
226 if (!changed) {
227 colors = Arrays.copyOf(colors, colors.length);
228 }
229 colors[i] = processColor(colors[i]);
230 changed = true;
231 }
232 }
233 if (changed) {
234 return new TextAppearanceSpan(
235 span.getFamily(), span.getTextStyle(), span.getTextSize(),
236 new ColorStateList(colorStateList.getStates(), colors),
237 span.getLinkTextColor());
238 }
239 }
240 return span;
241 }
242
243 private int processColor(int color) {
244 return Color.argb(Color.alpha(color),
245 255 - Color.red(color),
246 255 - Color.green(color),
247 255 - Color.blue(color));
248 }
Adrian Roos4ff3b122016-02-01 12:26:13 -0800249
250 /**
251 * Finds a suitable color such that there's enough contrast.
252 *
253 * @param color the color to start searching from.
254 * @param other the color to ensure contrast against. Assumed to be lighter than {@param color}
255 * @param findFg if true, we assume {@param color} is a foreground, otherwise a background.
256 * @param minRatio the minimum contrast ratio required.
257 * @return a color with the same hue as {@param color}, potentially darkened to meet the
258 * contrast ratio.
259 */
Selim Cinek5fb73f82017-04-20 16:55:38 -0700260 public static int findContrastColor(int color, int other, boolean findFg, double minRatio) {
Adrian Roos4ff3b122016-02-01 12:26:13 -0800261 int fg = findFg ? color : other;
262 int bg = findFg ? other : color;
263 if (ColorUtilsFromCompat.calculateContrast(fg, bg) >= minRatio) {
264 return color;
265 }
266
267 double[] lab = new double[3];
268 ColorUtilsFromCompat.colorToLAB(findFg ? fg : bg, lab);
269
270 double low = 0, high = lab[0];
271 final double a = lab[1], b = lab[2];
272 for (int i = 0; i < 15 && high - low > 0.00001; i++) {
273 final double l = (low + high) / 2;
274 if (findFg) {
275 fg = ColorUtilsFromCompat.LABToColor(l, a, b);
276 } else {
277 bg = ColorUtilsFromCompat.LABToColor(l, a, b);
278 }
279 if (ColorUtilsFromCompat.calculateContrast(fg, bg) > minRatio) {
280 low = l;
281 } else {
282 high = l;
283 }
284 }
285 return ColorUtilsFromCompat.LABToColor(low, a, b);
286 }
287
288 /**
Selim Cinekac5f0272017-05-02 16:05:41 -0700289 * Finds a suitable alpha such that there's enough contrast.
290 *
291 * @param color the color to start searching from.
292 * @param backgroundColor the color to ensure contrast against.
293 * @param minRatio the minimum contrast ratio required.
294 * @return the same color as {@param color} with potentially modified alpha to meet contrast
295 */
296 public static int findAlphaToMeetContrast(int color, int backgroundColor, double minRatio) {
297 int fg = color;
298 int bg = backgroundColor;
299 if (ColorUtilsFromCompat.calculateContrast(fg, bg) >= minRatio) {
300 return color;
301 }
302 int startAlpha = Color.alpha(color);
303 int r = Color.red(color);
304 int g = Color.green(color);
305 int b = Color.blue(color);
306
307 int low = startAlpha, high = 255;
308 for (int i = 0; i < 15 && high - low > 0; i++) {
309 final int alpha = (low + high) / 2;
310 fg = Color.argb(alpha, r, g, b);
311 if (ColorUtilsFromCompat.calculateContrast(fg, bg) > minRatio) {
312 high = alpha;
313 } else {
314 low = alpha;
315 }
316 }
317 return Color.argb(high, r, g, b);
318 }
319
320 /**
Adrian Roosf9d13f62016-11-08 15:42:20 -0800321 * Finds a suitable color such that there's enough contrast.
322 *
323 * @param color the color to start searching from.
324 * @param other the color to ensure contrast against. Assumed to be darker than {@param color}
325 * @param findFg if true, we assume {@param color} is a foreground, otherwise a background.
326 * @param minRatio the minimum contrast ratio required.
327 * @return a color with the same hue as {@param color}, potentially darkened to meet the
328 * contrast ratio.
329 */
330 public static int findContrastColorAgainstDark(int color, int other, boolean findFg,
331 double minRatio) {
332 int fg = findFg ? color : other;
333 int bg = findFg ? other : color;
334 if (ColorUtilsFromCompat.calculateContrast(fg, bg) >= minRatio) {
335 return color;
336 }
337
Adrian Roos99d2e642017-02-09 14:58:44 +0100338 float[] hsl = new float[3];
339 ColorUtilsFromCompat.colorToHSL(findFg ? fg : bg, hsl);
Adrian Roosf9d13f62016-11-08 15:42:20 -0800340
Adrian Roos99d2e642017-02-09 14:58:44 +0100341 float low = hsl[2], high = 1;
Adrian Roosf9d13f62016-11-08 15:42:20 -0800342 for (int i = 0; i < 15 && high - low > 0.00001; i++) {
Adrian Roos99d2e642017-02-09 14:58:44 +0100343 final float l = (low + high) / 2;
344 hsl[2] = l;
Adrian Roosf9d13f62016-11-08 15:42:20 -0800345 if (findFg) {
Adrian Roos99d2e642017-02-09 14:58:44 +0100346 fg = ColorUtilsFromCompat.HSLToColor(hsl);
Adrian Roosf9d13f62016-11-08 15:42:20 -0800347 } else {
Adrian Roos99d2e642017-02-09 14:58:44 +0100348 bg = ColorUtilsFromCompat.HSLToColor(hsl);
Adrian Roosf9d13f62016-11-08 15:42:20 -0800349 }
350 if (ColorUtilsFromCompat.calculateContrast(fg, bg) > minRatio) {
351 high = l;
352 } else {
353 low = l;
354 }
355 }
Adrian Roos99d2e642017-02-09 14:58:44 +0100356 return findFg ? fg : bg;
Adrian Roosf9d13f62016-11-08 15:42:20 -0800357 }
358
Adrian Roos487374f2017-01-11 15:48:14 -0800359 public static int ensureTextContrastOnBlack(int color) {
360 return findContrastColorAgainstDark(color, Color.BLACK, true /* fg */, 12);
361 }
362
Anthony Chenad4d1582017-04-10 16:07:58 -0700363 /**
364 * Finds a large text color with sufficient contrast over bg that has the same or darker hue as
365 * the original color, depending on the value of {@code isBgDarker}.
366 *
367 * @param isBgDarker {@code true} if {@code bg} is darker than {@code color}.
Adrian Roos4ff3b122016-02-01 12:26:13 -0800368 */
Anthony Chenad4d1582017-04-10 16:07:58 -0700369 public static int ensureLargeTextContrast(int color, int bg, boolean isBgDarker) {
370 return isBgDarker
371 ? findContrastColorAgainstDark(color, bg, true, 3)
372 : findContrastColor(color, bg, true, 3);
Adrian Roos4ff3b122016-02-01 12:26:13 -0800373 }
374
375 /**
Anthony Chenad4d1582017-04-10 16:07:58 -0700376 * Finds a text color with sufficient contrast over bg that has the same or darker hue as the
377 * original color, depending on the value of {@code isBgDarker}.
378 *
379 * @param isBgDarker {@code true} if {@code bg} is darker than {@code color}.
Adrian Roos4ff3b122016-02-01 12:26:13 -0800380 */
Anthony Chenad4d1582017-04-10 16:07:58 -0700381 private static int ensureTextContrast(int color, int bg, boolean isBgDarker) {
382 return isBgDarker
383 ? findContrastColorAgainstDark(color, bg, true, 4.5)
384 : findContrastColor(color, bg, true, 4.5);
Adrian Roos4ff3b122016-02-01 12:26:13 -0800385 }
386
387 /** Finds a background color for a text view with given text color and hint text color, that
388 * has the same hue as the original color.
389 */
390 public static int ensureTextBackgroundColor(int color, int textColor, int hintColor) {
391 color = findContrastColor(color, hintColor, false, 3.0);
392 return findContrastColor(color, textColor, false, 4.5);
393 }
394
395 private static String contrastChange(int colorOld, int colorNew, int bg) {
396 return String.format("from %.2f:1 to %.2f:1",
397 ColorUtilsFromCompat.calculateContrast(colorOld, bg),
398 ColorUtilsFromCompat.calculateContrast(colorNew, bg));
399 }
400
401 /**
402 * Resolves {@param color} to an actual color if it is {@link Notification#COLOR_DEFAULT}
403 */
404 public static int resolveColor(Context context, int color) {
405 if (color == Notification.COLOR_DEFAULT) {
406 return context.getColor(com.android.internal.R.color.notification_icon_default_color);
407 }
408 return color;
409 }
410
411 /**
412 * Resolves a Notification's color such that it has enough contrast to be used as the
Anthony Chenad4d1582017-04-10 16:07:58 -0700413 * color for the Notification's action and header text on a background that is lighter than
414 * {@code notificationColor}.
415 *
416 * @see {@link #resolveContrastColor(Context, int, boolean)}
417 */
418 public static int resolveContrastColor(Context context, int notificationColor,
419 int backgroundColor) {
420 return NotificationColorUtil.resolveContrastColor(context, notificationColor,
421 backgroundColor, false /* isDark */);
422 }
423
424 /**
425 * Resolves a Notification's color such that it has enough contrast to be used as the
Adrian Roos4ff3b122016-02-01 12:26:13 -0800426 * color for the Notification's action and header text.
427 *
428 * @param notificationColor the color of the notification or {@link Notification#COLOR_DEFAULT}
Selim Cinekac5f0272017-05-02 16:05:41 -0700429 * @param backgroundColor the background color to ensure the contrast against.
Anthony Chenad4d1582017-04-10 16:07:58 -0700430 * @param isDark whether or not the {@code notificationColor} will be placed on a background
431 * that is darker than the color itself
Adrian Roos4ff3b122016-02-01 12:26:13 -0800432 * @return a color of the same hue with enough contrast against the backgrounds.
433 */
Selim Cinekac5f0272017-05-02 16:05:41 -0700434 public static int resolveContrastColor(Context context, int notificationColor,
Anthony Chenad4d1582017-04-10 16:07:58 -0700435 int backgroundColor, boolean isDark) {
Adrian Roos4ff3b122016-02-01 12:26:13 -0800436 final int resolvedColor = resolveColor(context, notificationColor);
437
438 final int actionBg = context.getColor(
439 com.android.internal.R.color.notification_action_list);
Adrian Roos4ff3b122016-02-01 12:26:13 -0800440
441 int color = resolvedColor;
Anthony Chenad4d1582017-04-10 16:07:58 -0700442 color = NotificationColorUtil.ensureLargeTextContrast(color, actionBg, isDark);
443 color = NotificationColorUtil.ensureTextContrast(color, backgroundColor, isDark);
Adrian Roos4ff3b122016-02-01 12:26:13 -0800444
445 if (color != resolvedColor) {
446 if (DEBUG){
447 Log.w(TAG, String.format(
448 "Enhanced contrast of notification for %s %s (over action)"
449 + " and %s (over background) by changing #%s to %s",
450 context.getPackageName(),
451 NotificationColorUtil.contrastChange(resolvedColor, color, actionBg),
Selim Cinekac5f0272017-05-02 16:05:41 -0700452 NotificationColorUtil.contrastChange(resolvedColor, color, backgroundColor),
Adrian Roos4ff3b122016-02-01 12:26:13 -0800453 Integer.toHexString(resolvedColor), Integer.toHexString(color)));
454 }
455 }
456 return color;
457 }
458
459 /**
Selim Cinek5fb73f82017-04-20 16:55:38 -0700460 * Change a color by a specified value
Selim Cinek06e9e1f2016-07-08 17:14:16 -0700461 * @param baseColor the base color to lighten
462 * @param amount the amount to lighten the color from 0 to 100. This corresponds to the L
Selim Cinek5fb73f82017-04-20 16:55:38 -0700463 * increase in the LAB color space. A negative value will darken the color and
464 * a positive will lighten it.
465 * @return the changed color
Selim Cinek06e9e1f2016-07-08 17:14:16 -0700466 */
Selim Cinek5fb73f82017-04-20 16:55:38 -0700467 public static int changeColorLightness(int baseColor, int amount) {
Selim Cinek06e9e1f2016-07-08 17:14:16 -0700468 final double[] result = ColorUtilsFromCompat.getTempDouble3Array();
469 ColorUtilsFromCompat.colorToLAB(baseColor, result);
Selim Cinek5fb73f82017-04-20 16:55:38 -0700470 result[0] = Math.max(Math.min(100, result[0] + amount), 0);
Selim Cinek06e9e1f2016-07-08 17:14:16 -0700471 return ColorUtilsFromCompat.LABToColor(result[0], result[1], result[2]);
472 }
473
Adrian Roos487374f2017-01-11 15:48:14 -0800474 public static int resolveAmbientColor(Context context, int notificationColor) {
475 final int resolvedColor = resolveColor(context, notificationColor);
476
477 int color = resolvedColor;
478 color = NotificationColorUtil.ensureTextContrastOnBlack(color);
479
480 if (color != resolvedColor) {
481 if (DEBUG){
482 Log.w(TAG, String.format(
483 "Ambient contrast of notification for %s is %s (over black)"
484 + " by changing #%s to #%s",
485 context.getPackageName(),
486 NotificationColorUtil.contrastChange(resolvedColor, color, Color.BLACK),
487 Integer.toHexString(resolvedColor), Integer.toHexString(color)));
488 }
489 }
490 return color;
491 }
492
Selim Cinek7b9605b2017-01-19 17:36:00 -0800493 public static int resolvePrimaryColor(Context context, int backgroundColor) {
494 boolean useDark = shouldUseDark(backgroundColor);
495 if (useDark) {
496 return context.getColor(
497 com.android.internal.R.color.notification_primary_text_color_light);
498 } else {
499 return context.getColor(
500 com.android.internal.R.color.notification_primary_text_color_dark);
501 }
502 }
503
504 public static int resolveSecondaryColor(Context context, int backgroundColor) {
505 boolean useDark = shouldUseDark(backgroundColor);
506 if (useDark) {
507 return context.getColor(
508 com.android.internal.R.color.notification_secondary_text_color_light);
509 } else {
510 return context.getColor(
511 com.android.internal.R.color.notification_secondary_text_color_dark);
512 }
513 }
514
Selim Cinek875ba9b2017-02-13 16:20:17 -0800515 public static int resolveActionBarColor(Context context, int backgroundColor) {
516 if (backgroundColor == Notification.COLOR_DEFAULT) {
517 return context.getColor(com.android.internal.R.color.notification_action_list);
518 }
Selim Cinek622c64a2017-04-17 17:10:05 -0700519 return getShiftedColor(backgroundColor, 7);
520 }
521
522 /**
523 * Get a color that stays in the same tint, but darkens or lightens it by a certain
524 * amount.
525 * This also looks at the lightness of the provided color and shifts it appropriately.
526 *
527 * @param color the base color to use
528 * @param amount the amount from 1 to 100 how much to modify the color
529 * @return the now color that was modified
530 */
531 public static int getShiftedColor(int color, int amount) {
Selim Cinek7b9605b2017-01-19 17:36:00 -0800532 final double[] result = ColorUtilsFromCompat.getTempDouble3Array();
Selim Cinek622c64a2017-04-17 17:10:05 -0700533 ColorUtilsFromCompat.colorToLAB(color, result);
534 if (result[0] >= 4) {
535 result[0] = Math.max(0, result[0] - amount);
Selim Cinek7b9605b2017-01-19 17:36:00 -0800536 } else {
Selim Cinek622c64a2017-04-17 17:10:05 -0700537 result[0] = Math.min(100, result[0] + amount);
Selim Cinek7b9605b2017-01-19 17:36:00 -0800538 }
539 return ColorUtilsFromCompat.LABToColor(result[0], result[1], result[2]);
540 }
541
542 private static boolean shouldUseDark(int backgroundColor) {
543 boolean useDark = backgroundColor == Notification.COLOR_DEFAULT;
544 if (!useDark) {
545 useDark = ColorUtilsFromCompat.calculateLuminance(backgroundColor) > 0.5;
546 }
547 return useDark;
548 }
549
Selim Cinek5fb73f82017-04-20 16:55:38 -0700550 public static double calculateLuminance(int backgroundColor) {
551 return ColorUtilsFromCompat.calculateLuminance(backgroundColor);
552 }
553
554
555 public static double calculateContrast(int foregroundColor, int backgroundColor) {
556 return ColorUtilsFromCompat.calculateContrast(foregroundColor, backgroundColor);
557 }
558
Selim Cinek389edcd2017-05-11 19:16:44 -0700559 public static boolean satisfiesTextContrast(int backgroundColor, int foregroundColor) {
Selim Cinek4c807912017-06-23 17:22:38 -0700560 return NotificationColorUtil.calculateContrast(foregroundColor, backgroundColor) >= 4.5;
Selim Cinek389edcd2017-05-11 19:16:44 -0700561 }
562
Selim Cinek06e9e1f2016-07-08 17:14:16 -0700563 /**
Selim Cinekac5f0272017-05-02 16:05:41 -0700564 * Composite two potentially translucent colors over each other and returns the result.
565 */
566 public static int compositeColors(int foreground, int background) {
567 return ColorUtilsFromCompat.compositeColors(foreground, background);
568 }
569
Selim Cinek389edcd2017-05-11 19:16:44 -0700570 public static boolean isColorLight(int backgroundColor) {
571 return calculateLuminance(backgroundColor) > 0.5f;
572 }
573
Selim Cinekac5f0272017-05-02 16:05:41 -0700574 /**
Adrian Roos4ff3b122016-02-01 12:26:13 -0800575 * Framework copy of functions needed from android.support.v4.graphics.ColorUtils.
576 */
577 private static class ColorUtilsFromCompat {
578 private static final double XYZ_WHITE_REFERENCE_X = 95.047;
579 private static final double XYZ_WHITE_REFERENCE_Y = 100;
580 private static final double XYZ_WHITE_REFERENCE_Z = 108.883;
581 private static final double XYZ_EPSILON = 0.008856;
582 private static final double XYZ_KAPPA = 903.3;
583
584 private static final int MIN_ALPHA_SEARCH_MAX_ITERATIONS = 10;
585 private static final int MIN_ALPHA_SEARCH_PRECISION = 1;
586
587 private static final ThreadLocal<double[]> TEMP_ARRAY = new ThreadLocal<>();
588
589 private ColorUtilsFromCompat() {}
590
591 /**
592 * Composite two potentially translucent colors over each other and returns the result.
593 */
594 public static int compositeColors(@ColorInt int foreground, @ColorInt int background) {
595 int bgAlpha = Color.alpha(background);
596 int fgAlpha = Color.alpha(foreground);
597 int a = compositeAlpha(fgAlpha, bgAlpha);
598
599 int r = compositeComponent(Color.red(foreground), fgAlpha,
600 Color.red(background), bgAlpha, a);
601 int g = compositeComponent(Color.green(foreground), fgAlpha,
602 Color.green(background), bgAlpha, a);
603 int b = compositeComponent(Color.blue(foreground), fgAlpha,
604 Color.blue(background), bgAlpha, a);
605
606 return Color.argb(a, r, g, b);
607 }
608
609 private static int compositeAlpha(int foregroundAlpha, int backgroundAlpha) {
610 return 0xFF - (((0xFF - backgroundAlpha) * (0xFF - foregroundAlpha)) / 0xFF);
611 }
612
613 private static int compositeComponent(int fgC, int fgA, int bgC, int bgA, int a) {
614 if (a == 0) return 0;
615 return ((0xFF * fgC * fgA) + (bgC * bgA * (0xFF - fgA))) / (a * 0xFF);
616 }
617
618 /**
619 * Returns the luminance of a color as a float between {@code 0.0} and {@code 1.0}.
620 * <p>Defined as the Y component in the XYZ representation of {@code color}.</p>
621 */
622 @FloatRange(from = 0.0, to = 1.0)
623 public static double calculateLuminance(@ColorInt int color) {
624 final double[] result = getTempDouble3Array();
625 colorToXYZ(color, result);
626 // Luminance is the Y component
627 return result[1] / 100;
628 }
629
630 /**
631 * Returns the contrast ratio between {@code foreground} and {@code background}.
632 * {@code background} must be opaque.
633 * <p>
634 * Formula defined
635 * <a href="http://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef">here</a>.
636 */
637 public static double calculateContrast(@ColorInt int foreground, @ColorInt int background) {
638 if (Color.alpha(background) != 255) {
Selim Cinek4c807912017-06-23 17:22:38 -0700639 Log.wtf(TAG, "background can not be translucent: #"
Adrian Roos4ff3b122016-02-01 12:26:13 -0800640 + Integer.toHexString(background));
641 }
642 if (Color.alpha(foreground) < 255) {
643 // If the foreground is translucent, composite the foreground over the background
644 foreground = compositeColors(foreground, background);
645 }
646
647 final double luminance1 = calculateLuminance(foreground) + 0.05;
648 final double luminance2 = calculateLuminance(background) + 0.05;
649
650 // Now return the lighter luminance divided by the darker luminance
651 return Math.max(luminance1, luminance2) / Math.min(luminance1, luminance2);
652 }
653
654 /**
655 * Convert the ARGB color to its CIE Lab representative components.
656 *
657 * @param color the ARGB color to convert. The alpha component is ignored
658 * @param outLab 3-element array which holds the resulting LAB components
659 */
660 public static void colorToLAB(@ColorInt int color, @NonNull double[] outLab) {
661 RGBToLAB(Color.red(color), Color.green(color), Color.blue(color), outLab);
662 }
663
664 /**
665 * Convert RGB components to its CIE Lab representative components.
666 *
667 * <ul>
Selim Cinek06e9e1f2016-07-08 17:14:16 -0700668 * <li>outLab[0] is L [0 ...100)</li>
Adrian Roos4ff3b122016-02-01 12:26:13 -0800669 * <li>outLab[1] is a [-128...127)</li>
670 * <li>outLab[2] is b [-128...127)</li>
671 * </ul>
672 *
673 * @param r red component value [0..255]
674 * @param g green component value [0..255]
675 * @param b blue component value [0..255]
676 * @param outLab 3-element array which holds the resulting LAB components
677 */
678 public static void RGBToLAB(@IntRange(from = 0x0, to = 0xFF) int r,
679 @IntRange(from = 0x0, to = 0xFF) int g, @IntRange(from = 0x0, to = 0xFF) int b,
680 @NonNull double[] outLab) {
681 // First we convert RGB to XYZ
682 RGBToXYZ(r, g, b, outLab);
683 // outLab now contains XYZ
684 XYZToLAB(outLab[0], outLab[1], outLab[2], outLab);
685 // outLab now contains LAB representation
686 }
687
688 /**
689 * Convert the ARGB color to it's CIE XYZ representative components.
690 *
691 * <p>The resulting XYZ representation will use the D65 illuminant and the CIE
692 * 2° Standard Observer (1931).</p>
693 *
694 * <ul>
695 * <li>outXyz[0] is X [0 ...95.047)</li>
696 * <li>outXyz[1] is Y [0...100)</li>
697 * <li>outXyz[2] is Z [0...108.883)</li>
698 * </ul>
699 *
700 * @param color the ARGB color to convert. The alpha component is ignored
701 * @param outXyz 3-element array which holds the resulting LAB components
702 */
703 public static void colorToXYZ(@ColorInt int color, @NonNull double[] outXyz) {
704 RGBToXYZ(Color.red(color), Color.green(color), Color.blue(color), outXyz);
705 }
706
707 /**
708 * Convert RGB components to it's CIE XYZ representative components.
709 *
710 * <p>The resulting XYZ representation will use the D65 illuminant and the CIE
711 * 2° Standard Observer (1931).</p>
712 *
713 * <ul>
714 * <li>outXyz[0] is X [0 ...95.047)</li>
715 * <li>outXyz[1] is Y [0...100)</li>
716 * <li>outXyz[2] is Z [0...108.883)</li>
717 * </ul>
718 *
719 * @param r red component value [0..255]
720 * @param g green component value [0..255]
721 * @param b blue component value [0..255]
722 * @param outXyz 3-element array which holds the resulting XYZ components
723 */
724 public static void RGBToXYZ(@IntRange(from = 0x0, to = 0xFF) int r,
725 @IntRange(from = 0x0, to = 0xFF) int g, @IntRange(from = 0x0, to = 0xFF) int b,
726 @NonNull double[] outXyz) {
727 if (outXyz.length != 3) {
728 throw new IllegalArgumentException("outXyz must have a length of 3.");
729 }
730
731 double sr = r / 255.0;
732 sr = sr < 0.04045 ? sr / 12.92 : Math.pow((sr + 0.055) / 1.055, 2.4);
733 double sg = g / 255.0;
734 sg = sg < 0.04045 ? sg / 12.92 : Math.pow((sg + 0.055) / 1.055, 2.4);
735 double sb = b / 255.0;
736 sb = sb < 0.04045 ? sb / 12.92 : Math.pow((sb + 0.055) / 1.055, 2.4);
737
738 outXyz[0] = 100 * (sr * 0.4124 + sg * 0.3576 + sb * 0.1805);
739 outXyz[1] = 100 * (sr * 0.2126 + sg * 0.7152 + sb * 0.0722);
740 outXyz[2] = 100 * (sr * 0.0193 + sg * 0.1192 + sb * 0.9505);
741 }
742
743 /**
744 * Converts a color from CIE XYZ to CIE Lab representation.
745 *
746 * <p>This method expects the XYZ representation to use the D65 illuminant and the CIE
747 * 2° Standard Observer (1931).</p>
748 *
749 * <ul>
Selim Cinek06e9e1f2016-07-08 17:14:16 -0700750 * <li>outLab[0] is L [0 ...100)</li>
Adrian Roos4ff3b122016-02-01 12:26:13 -0800751 * <li>outLab[1] is a [-128...127)</li>
752 * <li>outLab[2] is b [-128...127)</li>
753 * </ul>
754 *
755 * @param x X component value [0...95.047)
756 * @param y Y component value [0...100)
757 * @param z Z component value [0...108.883)
758 * @param outLab 3-element array which holds the resulting Lab components
759 */
760 public static void XYZToLAB(@FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_X) double x,
761 @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Y) double y,
762 @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Z) double z,
763 @NonNull double[] outLab) {
764 if (outLab.length != 3) {
765 throw new IllegalArgumentException("outLab must have a length of 3.");
766 }
767 x = pivotXyzComponent(x / XYZ_WHITE_REFERENCE_X);
768 y = pivotXyzComponent(y / XYZ_WHITE_REFERENCE_Y);
769 z = pivotXyzComponent(z / XYZ_WHITE_REFERENCE_Z);
770 outLab[0] = Math.max(0, 116 * y - 16);
771 outLab[1] = 500 * (x - y);
772 outLab[2] = 200 * (y - z);
773 }
774
775 /**
776 * Converts a color from CIE Lab to CIE XYZ representation.
777 *
778 * <p>The resulting XYZ representation will use the D65 illuminant and the CIE
779 * 2° Standard Observer (1931).</p>
780 *
781 * <ul>
782 * <li>outXyz[0] is X [0 ...95.047)</li>
783 * <li>outXyz[1] is Y [0...100)</li>
784 * <li>outXyz[2] is Z [0...108.883)</li>
785 * </ul>
786 *
787 * @param l L component value [0...100)
788 * @param a A component value [-128...127)
789 * @param b B component value [-128...127)
790 * @param outXyz 3-element array which holds the resulting XYZ components
791 */
792 public static void LABToXYZ(@FloatRange(from = 0f, to = 100) final double l,
793 @FloatRange(from = -128, to = 127) final double a,
794 @FloatRange(from = -128, to = 127) final double b,
795 @NonNull double[] outXyz) {
796 final double fy = (l + 16) / 116;
797 final double fx = a / 500 + fy;
798 final double fz = fy - b / 200;
799
800 double tmp = Math.pow(fx, 3);
801 final double xr = tmp > XYZ_EPSILON ? tmp : (116 * fx - 16) / XYZ_KAPPA;
802 final double yr = l > XYZ_KAPPA * XYZ_EPSILON ? Math.pow(fy, 3) : l / XYZ_KAPPA;
803
804 tmp = Math.pow(fz, 3);
805 final double zr = tmp > XYZ_EPSILON ? tmp : (116 * fz - 16) / XYZ_KAPPA;
806
807 outXyz[0] = xr * XYZ_WHITE_REFERENCE_X;
808 outXyz[1] = yr * XYZ_WHITE_REFERENCE_Y;
809 outXyz[2] = zr * XYZ_WHITE_REFERENCE_Z;
810 }
811
812 /**
813 * Converts a color from CIE XYZ to its RGB representation.
814 *
815 * <p>This method expects the XYZ representation to use the D65 illuminant and the CIE
816 * 2° Standard Observer (1931).</p>
817 *
818 * @param x X component value [0...95.047)
819 * @param y Y component value [0...100)
820 * @param z Z component value [0...108.883)
821 * @return int containing the RGB representation
822 */
823 @ColorInt
824 public static int XYZToColor(@FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_X) double x,
825 @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Y) double y,
826 @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Z) double z) {
827 double r = (x * 3.2406 + y * -1.5372 + z * -0.4986) / 100;
828 double g = (x * -0.9689 + y * 1.8758 + z * 0.0415) / 100;
829 double b = (x * 0.0557 + y * -0.2040 + z * 1.0570) / 100;
830
831 r = r > 0.0031308 ? 1.055 * Math.pow(r, 1 / 2.4) - 0.055 : 12.92 * r;
832 g = g > 0.0031308 ? 1.055 * Math.pow(g, 1 / 2.4) - 0.055 : 12.92 * g;
833 b = b > 0.0031308 ? 1.055 * Math.pow(b, 1 / 2.4) - 0.055 : 12.92 * b;
834
835 return Color.rgb(
836 constrain((int) Math.round(r * 255), 0, 255),
837 constrain((int) Math.round(g * 255), 0, 255),
838 constrain((int) Math.round(b * 255), 0, 255));
839 }
840
841 /**
842 * Converts a color from CIE Lab to its RGB representation.
843 *
844 * @param l L component value [0...100]
845 * @param a A component value [-128...127]
846 * @param b B component value [-128...127]
847 * @return int containing the RGB representation
848 */
849 @ColorInt
850 public static int LABToColor(@FloatRange(from = 0f, to = 100) final double l,
851 @FloatRange(from = -128, to = 127) final double a,
852 @FloatRange(from = -128, to = 127) final double b) {
853 final double[] result = getTempDouble3Array();
854 LABToXYZ(l, a, b, result);
855 return XYZToColor(result[0], result[1], result[2]);
856 }
857
858 private static int constrain(int amount, int low, int high) {
859 return amount < low ? low : (amount > high ? high : amount);
860 }
861
Adrian Roos99d2e642017-02-09 14:58:44 +0100862 private static float constrain(float amount, float low, float high) {
863 return amount < low ? low : (amount > high ? high : amount);
864 }
865
Adrian Roos4ff3b122016-02-01 12:26:13 -0800866 private static double pivotXyzComponent(double component) {
867 return component > XYZ_EPSILON
868 ? Math.pow(component, 1 / 3.0)
869 : (XYZ_KAPPA * component + 16) / 116;
870 }
871
Selim Cinek06e9e1f2016-07-08 17:14:16 -0700872 public static double[] getTempDouble3Array() {
Adrian Roos4ff3b122016-02-01 12:26:13 -0800873 double[] result = TEMP_ARRAY.get();
874 if (result == null) {
875 result = new double[3];
876 TEMP_ARRAY.set(result);
877 }
878 return result;
879 }
880
Adrian Roos99d2e642017-02-09 14:58:44 +0100881 /**
882 * Convert HSL (hue-saturation-lightness) components to a RGB color.
883 * <ul>
884 * <li>hsl[0] is Hue [0 .. 360)</li>
885 * <li>hsl[1] is Saturation [0...1]</li>
886 * <li>hsl[2] is Lightness [0...1]</li>
887 * </ul>
888 * If hsv values are out of range, they are pinned.
889 *
890 * @param hsl 3-element array which holds the input HSL components
891 * @return the resulting RGB color
892 */
893 @ColorInt
894 public static int HSLToColor(@NonNull float[] hsl) {
895 final float h = hsl[0];
896 final float s = hsl[1];
897 final float l = hsl[2];
898
899 final float c = (1f - Math.abs(2 * l - 1f)) * s;
900 final float m = l - 0.5f * c;
901 final float x = c * (1f - Math.abs((h / 60f % 2f) - 1f));
902
903 final int hueSegment = (int) h / 60;
904
905 int r = 0, g = 0, b = 0;
906
907 switch (hueSegment) {
908 case 0:
909 r = Math.round(255 * (c + m));
910 g = Math.round(255 * (x + m));
911 b = Math.round(255 * m);
912 break;
913 case 1:
914 r = Math.round(255 * (x + m));
915 g = Math.round(255 * (c + m));
916 b = Math.round(255 * m);
917 break;
918 case 2:
919 r = Math.round(255 * m);
920 g = Math.round(255 * (c + m));
921 b = Math.round(255 * (x + m));
922 break;
923 case 3:
924 r = Math.round(255 * m);
925 g = Math.round(255 * (x + m));
926 b = Math.round(255 * (c + m));
927 break;
928 case 4:
929 r = Math.round(255 * (x + m));
930 g = Math.round(255 * m);
931 b = Math.round(255 * (c + m));
932 break;
933 case 5:
934 case 6:
935 r = Math.round(255 * (c + m));
936 g = Math.round(255 * m);
937 b = Math.round(255 * (x + m));
938 break;
939 }
940
941 r = constrain(r, 0, 255);
942 g = constrain(g, 0, 255);
943 b = constrain(b, 0, 255);
944
945 return Color.rgb(r, g, b);
946 }
947
948 /**
949 * Convert the ARGB color to its HSL (hue-saturation-lightness) components.
950 * <ul>
951 * <li>outHsl[0] is Hue [0 .. 360)</li>
952 * <li>outHsl[1] is Saturation [0...1]</li>
953 * <li>outHsl[2] is Lightness [0...1]</li>
954 * </ul>
955 *
956 * @param color the ARGB color to convert. The alpha component is ignored
957 * @param outHsl 3-element array which holds the resulting HSL components
958 */
959 public static void colorToHSL(@ColorInt int color, @NonNull float[] outHsl) {
960 RGBToHSL(Color.red(color), Color.green(color), Color.blue(color), outHsl);
961 }
962
963 /**
964 * Convert RGB components to HSL (hue-saturation-lightness).
965 * <ul>
966 * <li>outHsl[0] is Hue [0 .. 360)</li>
967 * <li>outHsl[1] is Saturation [0...1]</li>
968 * <li>outHsl[2] is Lightness [0...1]</li>
969 * </ul>
970 *
971 * @param r red component value [0..255]
972 * @param g green component value [0..255]
973 * @param b blue component value [0..255]
974 * @param outHsl 3-element array which holds the resulting HSL components
975 */
976 public static void RGBToHSL(@IntRange(from = 0x0, to = 0xFF) int r,
977 @IntRange(from = 0x0, to = 0xFF) int g, @IntRange(from = 0x0, to = 0xFF) int b,
978 @NonNull float[] outHsl) {
979 final float rf = r / 255f;
980 final float gf = g / 255f;
981 final float bf = b / 255f;
982
983 final float max = Math.max(rf, Math.max(gf, bf));
984 final float min = Math.min(rf, Math.min(gf, bf));
985 final float deltaMaxMin = max - min;
986
987 float h, s;
988 float l = (max + min) / 2f;
989
990 if (max == min) {
991 // Monochromatic
992 h = s = 0f;
993 } else {
994 if (max == rf) {
995 h = ((gf - bf) / deltaMaxMin) % 6f;
996 } else if (max == gf) {
997 h = ((bf - rf) / deltaMaxMin) + 2f;
998 } else {
999 h = ((rf - gf) / deltaMaxMin) + 4f;
1000 }
1001
1002 s = deltaMaxMin / (1f - Math.abs(2f * l - 1f));
1003 }
1004
1005 h = (h * 60f) % 360f;
1006 if (h < 0) {
1007 h += 360f;
1008 }
1009
1010 outHsl[0] = constrain(h, 0f, 360f);
1011 outHsl[1] = constrain(s, 0f, 1f);
1012 outHsl[2] = constrain(l, 0f, 1f);
1013 }
1014
Adrian Roos4ff3b122016-02-01 12:26:13 -08001015 }
Jorim Jaggi5c2d8462014-03-21 17:37:00 +01001016}