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