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.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 
     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