Home | History | Annotate | Download | only in util
      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 
     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