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.VectorDrawable;
28import 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/**
38 * Helper class to process legacy (Holo) notifications to make them look like material notifications.
39 *
40 * @hide
41 */
42public 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
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
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     */
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     */
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     */
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     */
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
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
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