blob: 665055cbb51c2ac444cdd925aea46ca377f73615 [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
19import android.content.Context;
20import android.content.res.ColorStateList;
21import android.content.res.Resources;
22import android.graphics.Bitmap;
23import android.graphics.Color;
24import android.graphics.drawable.AnimationDrawable;
25import android.graphics.drawable.BitmapDrawable;
26import android.graphics.drawable.Drawable;
Dan Sandler26e81cf2014-05-06 10:01:27 -040027import android.graphics.drawable.VectorDrawable;
Jorim Jaggi5c2d8462014-03-21 17:37:00 +010028import android.text.SpannableStringBuilder;
29import android.text.Spanned;
30import android.text.style.TextAppearanceSpan;
31import android.util.Log;
32import android.util.Pair;
33
34import java.util.Arrays;
35import java.util.WeakHashMap;
36
37/**
Alan Viverette830960c2014-06-06 15:48:55 -070038 * Helper class to process legacy (Holo) notifications to make them look like material notifications.
Jorim Jaggi5c2d8462014-03-21 17:37:00 +010039 *
40 * @hide
41 */
Dan Sandler26e81cf2014-05-06 10:01:27 -040042public class NotificationColorUtil {
Jorim Jaggi5c2d8462014-03-21 17:37:00 +010043
Dan Sandler26e81cf2014-05-06 10:01:27 -040044 private static final String TAG = "NotificationColorUtil";
Jorim Jaggi5c2d8462014-03-21 17:37:00 +010045
46 private static final Object sLock = new Object();
Dan Sandler26e81cf2014-05-06 10:01:27 -040047 private static NotificationColorUtil sInstance;
Jorim Jaggi5c2d8462014-03-21 17:37:00 +010048
49 private final ImageUtils mImageUtils = new ImageUtils();
50 private final WeakHashMap<Bitmap, Pair<Boolean, Integer>> mGrayscaleBitmapCache =
51 new WeakHashMap<Bitmap, Pair<Boolean, Integer>>();
52
Dan Sandler26e81cf2014-05-06 10:01:27 -040053 public static NotificationColorUtil getInstance() {
Jorim Jaggi5c2d8462014-03-21 17:37:00 +010054 synchronized (sLock) {
55 if (sInstance == null) {
Dan Sandler26e81cf2014-05-06 10:01:27 -040056 sInstance = new NotificationColorUtil();
Jorim Jaggi5c2d8462014-03-21 17:37:00 +010057 }
58 return sInstance;
59 }
60 }
61
62 /**
63 * Checks whether a bitmap is grayscale. Grayscale here means "very close to a perfect
64 * gray".
65 *
66 * @param bitmap The bitmap to test.
67 * @return Whether the bitmap is grayscale.
68 */
69 public boolean isGrayscale(Bitmap bitmap) {
70 synchronized (sLock) {
71 Pair<Boolean, Integer> cached = mGrayscaleBitmapCache.get(bitmap);
72 if (cached != null) {
73 if (cached.second == bitmap.getGenerationId()) {
74 return cached.first;
75 }
76 }
77 }
78 boolean result;
79 int generationId;
80 synchronized (mImageUtils) {
81 result = mImageUtils.isGrayscale(bitmap);
82
83 // generationId and the check whether the Bitmap is grayscale can't be read atomically
84 // here. However, since the thread is in the process of posting the notification, we can
85 // assume that it doesn't modify the bitmap while we are checking the pixels.
86 generationId = bitmap.getGenerationId();
87 }
88 synchronized (sLock) {
89 mGrayscaleBitmapCache.put(bitmap, Pair.create(result, generationId));
90 }
91 return result;
92 }
93
94 /**
95 * Checks whether a drawable is grayscale. Grayscale here means "very close to a perfect
96 * gray".
97 *
98 * @param d The drawable to test.
99 * @return Whether the drawable is grayscale.
100 */
101 public boolean isGrayscale(Drawable d) {
102 if (d == null) {
103 return false;
104 } else if (d instanceof BitmapDrawable) {
105 BitmapDrawable bd = (BitmapDrawable) d;
106 return bd.getBitmap() != null && isGrayscale(bd.getBitmap());
107 } else if (d instanceof AnimationDrawable) {
108 AnimationDrawable ad = (AnimationDrawable) d;
109 int count = ad.getNumberOfFrames();
110 return count > 0 && isGrayscale(ad.getFrame(0));
Dan Sandler26e81cf2014-05-06 10:01:27 -0400111 } else if (d instanceof VectorDrawable) {
112 // We just assume you're doing the right thing if using vectors
113 return true;
Jorim Jaggi5c2d8462014-03-21 17:37:00 +0100114 } else {
115 return false;
116 }
117 }
118
119 /**
120 * Checks whether a drawable with a resoure id is grayscale. Grayscale here means "very close
121 * to a perfect gray".
122 *
123 * @param context The context to load the drawable from.
124 * @return Whether the drawable is grayscale.
125 */
126 public boolean isGrayscale(Context context, int drawableResId) {
127 if (drawableResId != 0) {
128 try {
129 return isGrayscale(context.getDrawable(drawableResId));
130 } catch (Resources.NotFoundException ex) {
131 Log.e(TAG, "Drawable not found: " + drawableResId);
132 return false;
133 }
134 } else {
135 return false;
136 }
137 }
138
139 /**
140 * Inverts all the grayscale colors set by {@link android.text.style.TextAppearanceSpan}s on
141 * the text.
142 *
143 * @param charSequence The text to process.
144 * @return The color inverted text.
145 */
146 public CharSequence invertCharSequenceColors(CharSequence charSequence) {
147 if (charSequence instanceof Spanned) {
148 Spanned ss = (Spanned) charSequence;
149 Object[] spans = ss.getSpans(0, ss.length(), Object.class);
150 SpannableStringBuilder builder = new SpannableStringBuilder(ss.toString());
151 for (Object span : spans) {
152 Object resultSpan = span;
153 if (span instanceof TextAppearanceSpan) {
154 resultSpan = processTextAppearanceSpan((TextAppearanceSpan) span);
155 }
156 builder.setSpan(resultSpan, ss.getSpanStart(span), ss.getSpanEnd(span),
157 ss.getSpanFlags(span));
158 }
159 return builder;
160 }
161 return charSequence;
162 }
163
164 private TextAppearanceSpan processTextAppearanceSpan(TextAppearanceSpan span) {
165 ColorStateList colorStateList = span.getTextColor();
166 if (colorStateList != null) {
167 int[] colors = colorStateList.getColors();
168 boolean changed = false;
169 for (int i = 0; i < colors.length; i++) {
170 if (ImageUtils.isGrayscale(colors[i])) {
171
172 // Allocate a new array so we don't change the colors in the old color state
173 // list.
174 if (!changed) {
175 colors = Arrays.copyOf(colors, colors.length);
176 }
177 colors[i] = processColor(colors[i]);
178 changed = true;
179 }
180 }
181 if (changed) {
182 return new TextAppearanceSpan(
183 span.getFamily(), span.getTextStyle(), span.getTextSize(),
184 new ColorStateList(colorStateList.getStates(), colors),
185 span.getLinkTextColor());
186 }
187 }
188 return span;
189 }
190
191 private int processColor(int color) {
192 return Color.argb(Color.alpha(color),
193 255 - Color.red(color),
194 255 - Color.green(color),
195 255 - Color.blue(color));
196 }
197}