NotificationColorUtil.java revision 08a04c15245c970856022d0779aa27d4d63cdee3
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
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;
27import android.graphics.drawable.Icon;
28import android.graphics.drawable.VectorDrawable;
29import android.text.SpannableStringBuilder;
30import android.text.Spanned;
31import android.text.style.TextAppearanceSpan;
32import android.util.Log;
33import android.util.Pair;
34
35import java.util.Arrays;
36import java.util.WeakHashMap;
37
38/**
39 * Helper class to process legacy (Holo) notifications to make them look like material notifications.
40 *
41 * @hide
42 */
43public 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
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
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     */
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     */
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
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     */
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     */
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
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
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