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