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 getInstance(Context context)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 NotificationColorUtil(Context context)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 */ isGrayscaleIcon(Bitmap bitmap)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 */ isGrayscaleIcon(Drawable d)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 isGrayscaleIcon(Context context, Icon icon)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 */ isGrayscaleIcon(Context context, int drawableResId)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 */ invertCharSequenceColors(CharSequence charSequence)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 processTextAppearanceSpan(TextAppearanceSpan span)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 processColor(int color)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