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.content.Context;
20 import android.content.res.ColorStateList;
21 import android.content.res.Resources;
22 import android.graphics.Bitmap;
23 import android.graphics.Color;
24 import android.graphics.drawable.AnimationDrawable;
25 import android.graphics.drawable.BitmapDrawable;
26 import android.graphics.drawable.Drawable;
27 import android.graphics.drawable.Icon;
28 import android.graphics.drawable.VectorDrawable;
29 import android.text.SpannableStringBuilder;
30 import android.text.Spanned;
31 import android.text.style.TextAppearanceSpan;
32 import android.util.Log;
33 import android.util.Pair;
34 
35 import java.util.Arrays;
36 import java.util.WeakHashMap;
37 
38 /**
39  * Helper class to process legacy (Holo) notifications to make them look like material notifications.
40  *
41  * @hide
42  */
43 public class NotificationColorUtil {
44 
45     private static final String TAG = "NotificationColorUtil";
46 
47     private static final Object sLock = new Object();
48     private static NotificationColorUtil sInstance;
49 
50     private final ImageUtils mImageUtils = new ImageUtils();
51     private final WeakHashMap<Bitmap, Pair<Boolean, Integer>> mGrayscaleBitmapCache =
52             new WeakHashMap<Bitmap, Pair<Boolean, Integer>>();
53 
54     private final int mGrayscaleIconMaxSize; // @dimen/notification_large_icon_width (64dp)
55 
getInstance(Context context)56     public static NotificationColorUtil getInstance(Context context) {
57         synchronized (sLock) {
58             if (sInstance == null) {
59                 sInstance = new NotificationColorUtil(context);
60             }
61             return sInstance;
62         }
63     }
64 
NotificationColorUtil(Context context)65     private NotificationColorUtil(Context context) {
66         mGrayscaleIconMaxSize = context.getResources().getDimensionPixelSize(
67                 com.android.internal.R.dimen.notification_large_icon_width);
68     }
69 
70     /**
71      * Checks whether a Bitmap is a small grayscale icon.
72      * Grayscale here means "very close to a perfect gray"; icon means "no larger than 64dp".
73      *
74      * @param bitmap The bitmap to test.
75      * @return True if the bitmap is grayscale; false if it is color or too large to examine.
76      */
isGrayscaleIcon(Bitmap bitmap)77     public boolean isGrayscaleIcon(Bitmap bitmap) {
78         // quick test: reject large bitmaps
79         if (bitmap.getWidth() > mGrayscaleIconMaxSize
80                 || bitmap.getHeight() > mGrayscaleIconMaxSize) {
81             return false;
82         }
83 
84         synchronized (sLock) {
85             Pair<Boolean, Integer> cached = mGrayscaleBitmapCache.get(bitmap);
86             if (cached != null) {
87                 if (cached.second == bitmap.getGenerationId()) {
88                     return cached.first;
89                 }
90             }
91         }
92         boolean result;
93         int generationId;
94         synchronized (mImageUtils) {
95             result = mImageUtils.isGrayscale(bitmap);
96 
97             // generationId and the check whether the Bitmap is grayscale can't be read atomically
98             // here. However, since the thread is in the process of posting the notification, we can
99             // assume that it doesn't modify the bitmap while we are checking the pixels.
100             generationId = bitmap.getGenerationId();
101         }
102         synchronized (sLock) {
103             mGrayscaleBitmapCache.put(bitmap, Pair.create(result, generationId));
104         }
105         return result;
106     }
107 
108     /**
109      * Checks whether a Drawable is a small grayscale icon.
110      * Grayscale here means "very close to a perfect gray"; icon means "no larger than 64dp".
111      *
112      * @param d The drawable to test.
113      * @return True if the bitmap is grayscale; false if it is color or too large to examine.
114      */
isGrayscaleIcon(Drawable d)115     public boolean isGrayscaleIcon(Drawable d) {
116         if (d == null) {
117             return false;
118         } else if (d instanceof BitmapDrawable) {
119             BitmapDrawable bd = (BitmapDrawable) d;
120             return bd.getBitmap() != null && isGrayscaleIcon(bd.getBitmap());
121         } else if (d instanceof AnimationDrawable) {
122             AnimationDrawable ad = (AnimationDrawable) d;
123             int count = ad.getNumberOfFrames();
124             return count > 0 && isGrayscaleIcon(ad.getFrame(0));
125         } else if (d instanceof VectorDrawable) {
126             // We just assume you're doing the right thing if using vectors
127             return true;
128         } else {
129             return false;
130         }
131     }
132 
isGrayscaleIcon(Context context, Icon icon)133     public boolean isGrayscaleIcon(Context context, Icon icon) {
134         if (icon == null) {
135             return false;
136         }
137         switch (icon.getType()) {
138             case Icon.TYPE_BITMAP:
139                 return isGrayscaleIcon(icon.getBitmap());
140             case Icon.TYPE_RESOURCE:
141                 return isGrayscaleIcon(context, icon.getResId());
142             default:
143                 return false;
144         }
145     }
146 
147     /**
148      * Checks whether a drawable with a resoure id is a small grayscale icon.
149      * Grayscale here means "very close to a perfect gray"; icon means "no larger than 64dp".
150      *
151      * @param context The context to load the drawable from.
152      * @return True if the bitmap is grayscale; false if it is color or too large to examine.
153      */
isGrayscaleIcon(Context context, int drawableResId)154     public boolean isGrayscaleIcon(Context context, int drawableResId) {
155         if (drawableResId != 0) {
156             try {
157                 return isGrayscaleIcon(context.getDrawable(drawableResId));
158             } catch (Resources.NotFoundException ex) {
159                 Log.e(TAG, "Drawable not found: " + drawableResId);
160                 return false;
161             }
162         } else {
163             return false;
164         }
165     }
166 
167     /**
168      * Inverts all the grayscale colors set by {@link android.text.style.TextAppearanceSpan}s on
169      * the text.
170      *
171      * @param charSequence The text to process.
172      * @return The color inverted text.
173      */
invertCharSequenceColors(CharSequence charSequence)174     public CharSequence invertCharSequenceColors(CharSequence charSequence) {
175         if (charSequence instanceof Spanned) {
176             Spanned ss = (Spanned) charSequence;
177             Object[] spans = ss.getSpans(0, ss.length(), Object.class);
178             SpannableStringBuilder builder = new SpannableStringBuilder(ss.toString());
179             for (Object span : spans) {
180                 Object resultSpan = span;
181                 if (span instanceof TextAppearanceSpan) {
182                     resultSpan = processTextAppearanceSpan((TextAppearanceSpan) span);
183                 }
184                 builder.setSpan(resultSpan, ss.getSpanStart(span), ss.getSpanEnd(span),
185                         ss.getSpanFlags(span));
186             }
187             return builder;
188         }
189         return charSequence;
190     }
191 
processTextAppearanceSpan(TextAppearanceSpan span)192     private TextAppearanceSpan processTextAppearanceSpan(TextAppearanceSpan span) {
193         ColorStateList colorStateList = span.getTextColor();
194         if (colorStateList != null) {
195             int[] colors = colorStateList.getColors();
196             boolean changed = false;
197             for (int i = 0; i < colors.length; i++) {
198                 if (ImageUtils.isGrayscale(colors[i])) {
199 
200                     // Allocate a new array so we don't change the colors in the old color state
201                     // list.
202                     if (!changed) {
203                         colors = Arrays.copyOf(colors, colors.length);
204                     }
205                     colors[i] = processColor(colors[i]);
206                     changed = true;
207                 }
208             }
209             if (changed) {
210                 return new TextAppearanceSpan(
211                         span.getFamily(), span.getTextStyle(), span.getTextSize(),
212                         new ColorStateList(colorStateList.getStates(), colors),
213                         span.getLinkTextColor());
214             }
215         }
216         return span;
217     }
218 
processColor(int color)219     private int processColor(int color) {
220         return Color.argb(Color.alpha(color),
221                 255 - Color.red(color),
222                 255 - Color.green(color),
223                 255 - Color.blue(color));
224     }
225 }
226