1 /* 2 * Copyright (C) 2017 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 android.app; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.annotation.SystemApi; 22 import android.graphics.Bitmap; 23 import android.graphics.Canvas; 24 import android.graphics.Color; 25 import android.graphics.Rect; 26 import android.graphics.drawable.Drawable; 27 import android.os.Parcel; 28 import android.os.Parcelable; 29 import android.util.Log; 30 import android.util.Size; 31 32 import com.android.internal.graphics.ColorUtils; 33 import com.android.internal.graphics.palette.Palette; 34 import com.android.internal.graphics.palette.VariationalKMeansQuantizer; 35 import com.android.internal.util.ContrastColorUtil; 36 37 import java.io.FileOutputStream; 38 import java.util.ArrayList; 39 import java.util.Collections; 40 import java.util.List; 41 42 /** 43 * Provides information about the colors of a wallpaper. 44 * <p> 45 * Exposes the 3 most visually representative colors of a wallpaper. Can be either 46 * {@link WallpaperColors#getPrimaryColor()}, {@link WallpaperColors#getSecondaryColor()} 47 * or {@link WallpaperColors#getTertiaryColor()}. 48 */ 49 public final class WallpaperColors implements Parcelable { 50 51 private static final boolean DEBUG_DARK_PIXELS = false; 52 53 /** 54 * Specifies that dark text is preferred over the current wallpaper for best presentation. 55 * <p> 56 * eg. A launcher may set its text color to black if this flag is specified. 57 * @hide 58 */ 59 @SystemApi 60 public static final int HINT_SUPPORTS_DARK_TEXT = 1 << 0; 61 62 /** 63 * Specifies that dark theme is preferred over the current wallpaper for best presentation. 64 * <p> 65 * eg. A launcher may set its drawer color to black if this flag is specified. 66 * @hide 67 */ 68 @SystemApi 69 public static final int HINT_SUPPORTS_DARK_THEME = 1 << 1; 70 71 /** 72 * Specifies that this object was generated by extracting colors from a bitmap. 73 * @hide 74 */ 75 public static final int HINT_FROM_BITMAP = 1 << 2; 76 77 // Maximum size that a bitmap can have to keep our calculations sane 78 private static final int MAX_BITMAP_SIZE = 112; 79 80 // Even though we have a maximum size, we'll mainly match bitmap sizes 81 // using the area instead. This way our comparisons are aspect ratio independent. 82 private static final int MAX_WALLPAPER_EXTRACTION_AREA = MAX_BITMAP_SIZE * MAX_BITMAP_SIZE; 83 84 // When extracting the main colors, only consider colors 85 // present in at least MIN_COLOR_OCCURRENCE of the image 86 private static final float MIN_COLOR_OCCURRENCE = 0.05f; 87 88 // Decides when dark theme is optimal for this wallpaper 89 private static final float DARK_THEME_MEAN_LUMINANCE = 0.25f; 90 // Minimum mean luminosity that an image needs to have to support dark text 91 private static final float BRIGHT_IMAGE_MEAN_LUMINANCE = 0.75f; 92 // We also check if the image has dark pixels in it, 93 // to avoid bright images with some dark spots. 94 private static final float DARK_PIXEL_CONTRAST = 6f; 95 private static final float MAX_DARK_AREA = 0.025f; 96 97 private final ArrayList<Color> mMainColors; 98 private int mColorHints; 99 WallpaperColors(Parcel parcel)100 public WallpaperColors(Parcel parcel) { 101 mMainColors = new ArrayList<>(); 102 final int count = parcel.readInt(); 103 for (int i = 0; i < count; i++) { 104 final int colorInt = parcel.readInt(); 105 Color color = Color.valueOf(colorInt); 106 mMainColors.add(color); 107 } 108 mColorHints = parcel.readInt(); 109 } 110 111 /** 112 * Constructs {@link WallpaperColors} from a drawable. 113 * <p> 114 * Main colors will be extracted from the drawable. 115 * 116 * @param drawable Source where to extract from. 117 */ fromDrawable(Drawable drawable)118 public static WallpaperColors fromDrawable(Drawable drawable) { 119 if (drawable == null) { 120 throw new IllegalArgumentException("Drawable cannot be null"); 121 } 122 123 Rect initialBounds = drawable.copyBounds(); 124 int width = drawable.getIntrinsicWidth(); 125 int height = drawable.getIntrinsicHeight(); 126 127 // Some drawables do not have intrinsic dimensions 128 if (width <= 0 || height <= 0) { 129 width = MAX_BITMAP_SIZE; 130 height = MAX_BITMAP_SIZE; 131 } 132 133 Size optimalSize = calculateOptimalSize(width, height); 134 Bitmap bitmap = Bitmap.createBitmap(optimalSize.getWidth(), optimalSize.getHeight(), 135 Bitmap.Config.ARGB_8888); 136 final Canvas bmpCanvas = new Canvas(bitmap); 137 drawable.setBounds(0, 0, bitmap.getWidth(), bitmap.getHeight()); 138 drawable.draw(bmpCanvas); 139 140 final WallpaperColors colors = WallpaperColors.fromBitmap(bitmap); 141 bitmap.recycle(); 142 143 drawable.setBounds(initialBounds); 144 return colors; 145 } 146 147 /** 148 * Constructs {@link WallpaperColors} from a bitmap. 149 * <p> 150 * Main colors will be extracted from the bitmap. 151 * 152 * @param bitmap Source where to extract from. 153 */ fromBitmap(@onNull Bitmap bitmap)154 public static WallpaperColors fromBitmap(@NonNull Bitmap bitmap) { 155 if (bitmap == null) { 156 throw new IllegalArgumentException("Bitmap can't be null"); 157 } 158 159 final int bitmapArea = bitmap.getWidth() * bitmap.getHeight(); 160 boolean shouldRecycle = false; 161 if (bitmapArea > MAX_WALLPAPER_EXTRACTION_AREA) { 162 shouldRecycle = true; 163 Size optimalSize = calculateOptimalSize(bitmap.getWidth(), bitmap.getHeight()); 164 bitmap = Bitmap.createScaledBitmap(bitmap, optimalSize.getWidth(), 165 optimalSize.getHeight(), true /* filter */); 166 } 167 168 final Palette palette = Palette 169 .from(bitmap) 170 .setQuantizer(new VariationalKMeansQuantizer()) 171 .maximumColorCount(5) 172 .clearFilters() 173 .resizeBitmapArea(MAX_WALLPAPER_EXTRACTION_AREA) 174 .generate(); 175 176 // Remove insignificant colors and sort swatches by population 177 final ArrayList<Palette.Swatch> swatches = new ArrayList<>(palette.getSwatches()); 178 final float minColorArea = bitmap.getWidth() * bitmap.getHeight() * MIN_COLOR_OCCURRENCE; 179 swatches.removeIf(s -> s.getPopulation() < minColorArea); 180 swatches.sort((a, b) -> b.getPopulation() - a.getPopulation()); 181 182 final int swatchesSize = swatches.size(); 183 Color primary = null, secondary = null, tertiary = null; 184 185 swatchLoop: 186 for (int i = 0; i < swatchesSize; i++) { 187 Color color = Color.valueOf(swatches.get(i).getRgb()); 188 switch (i) { 189 case 0: 190 primary = color; 191 break; 192 case 1: 193 secondary = color; 194 break; 195 case 2: 196 tertiary = color; 197 break; 198 default: 199 // out of bounds 200 break swatchLoop; 201 } 202 } 203 204 int hints = calculateDarkHints(bitmap); 205 206 if (shouldRecycle) { 207 bitmap.recycle(); 208 } 209 210 return new WallpaperColors(primary, secondary, tertiary, HINT_FROM_BITMAP | hints); 211 } 212 213 /** 214 * Constructs a new object from three colors. 215 * 216 * @param primaryColor Primary color. 217 * @param secondaryColor Secondary color. 218 * @param tertiaryColor Tertiary color. 219 * @see WallpaperColors#fromBitmap(Bitmap) 220 * @see WallpaperColors#fromDrawable(Drawable) 221 */ WallpaperColors(@onNull Color primaryColor, @Nullable Color secondaryColor, @Nullable Color tertiaryColor)222 public WallpaperColors(@NonNull Color primaryColor, @Nullable Color secondaryColor, 223 @Nullable Color tertiaryColor) { 224 this(primaryColor, secondaryColor, tertiaryColor, 0); 225 } 226 227 /** 228 * Constructs a new object from three colors, where hints can be specified. 229 * 230 * @param primaryColor Primary color. 231 * @param secondaryColor Secondary color. 232 * @param tertiaryColor Tertiary color. 233 * @param colorHints A combination of WallpaperColor hints. 234 * @see WallpaperColors#HINT_SUPPORTS_DARK_TEXT 235 * @see WallpaperColors#fromBitmap(Bitmap) 236 * @see WallpaperColors#fromDrawable(Drawable) 237 * @hide 238 */ 239 @SystemApi WallpaperColors(@onNull Color primaryColor, @Nullable Color secondaryColor, @Nullable Color tertiaryColor, int colorHints)240 public WallpaperColors(@NonNull Color primaryColor, @Nullable Color secondaryColor, 241 @Nullable Color tertiaryColor, int colorHints) { 242 243 if (primaryColor == null) { 244 throw new IllegalArgumentException("Primary color should never be null."); 245 } 246 247 mMainColors = new ArrayList<>(3); 248 mMainColors.add(primaryColor); 249 if (secondaryColor != null) { 250 mMainColors.add(secondaryColor); 251 } 252 if (tertiaryColor != null) { 253 if (secondaryColor == null) { 254 throw new IllegalArgumentException("tertiaryColor can't be specified when " 255 + "secondaryColor is null"); 256 } 257 mMainColors.add(tertiaryColor); 258 } 259 260 mColorHints = colorHints; 261 } 262 263 public static final @android.annotation.NonNull Creator<WallpaperColors> CREATOR = new Creator<WallpaperColors>() { 264 @Override 265 public WallpaperColors createFromParcel(Parcel in) { 266 return new WallpaperColors(in); 267 } 268 269 @Override 270 public WallpaperColors[] newArray(int size) { 271 return new WallpaperColors[size]; 272 } 273 }; 274 275 @Override describeContents()276 public int describeContents() { 277 return 0; 278 } 279 280 @Override writeToParcel(Parcel dest, int flags)281 public void writeToParcel(Parcel dest, int flags) { 282 List<Color> mainColors = getMainColors(); 283 int count = mainColors.size(); 284 dest.writeInt(count); 285 for (int i = 0; i < count; i++) { 286 Color color = mainColors.get(i); 287 dest.writeInt(color.toArgb()); 288 } 289 dest.writeInt(mColorHints); 290 } 291 292 /** 293 * Gets the most visually representative color of the wallpaper. 294 * "Visually representative" means easily noticeable in the image, 295 * probably happening at high frequency. 296 * 297 * @return A color. 298 */ getPrimaryColor()299 public @NonNull Color getPrimaryColor() { 300 return mMainColors.get(0); 301 } 302 303 /** 304 * Gets the second most preeminent color of the wallpaper. Can be null. 305 * 306 * @return A color, may be null. 307 */ getSecondaryColor()308 public @Nullable Color getSecondaryColor() { 309 return mMainColors.size() < 2 ? null : mMainColors.get(1); 310 } 311 312 /** 313 * Gets the third most preeminent color of the wallpaper. Can be null. 314 * 315 * @return A color, may be null. 316 */ getTertiaryColor()317 public @Nullable Color getTertiaryColor() { 318 return mMainColors.size() < 3 ? null : mMainColors.get(2); 319 } 320 321 /** 322 * List of most preeminent colors, sorted by importance. 323 * 324 * @return List of colors. 325 * @hide 326 */ getMainColors()327 public @NonNull List<Color> getMainColors() { 328 return Collections.unmodifiableList(mMainColors); 329 } 330 331 @Override equals(Object o)332 public boolean equals(Object o) { 333 if (o == null || getClass() != o.getClass()) { 334 return false; 335 } 336 337 WallpaperColors other = (WallpaperColors) o; 338 return mMainColors.equals(other.mMainColors) 339 && mColorHints == other.mColorHints; 340 } 341 342 @Override hashCode()343 public int hashCode() { 344 return 31 * mMainColors.hashCode() + mColorHints; 345 } 346 347 /** 348 * Combination of WallpaperColor hints. 349 * 350 * @see WallpaperColors#HINT_SUPPORTS_DARK_TEXT 351 * @return True if dark text is supported. 352 * @hide 353 */ 354 @SystemApi getColorHints()355 public int getColorHints() { 356 return mColorHints; 357 } 358 359 /** 360 * @param colorHints Combination of WallpaperColors hints. 361 * @see WallpaperColors#HINT_SUPPORTS_DARK_TEXT 362 * @hide 363 */ setColorHints(int colorHints)364 public void setColorHints(int colorHints) { 365 mColorHints = colorHints; 366 } 367 368 /** 369 * Checks if image is bright and clean enough to support light text. 370 * 371 * @param source What to read. 372 * @return Whether image supports dark text or not. 373 */ calculateDarkHints(Bitmap source)374 private static int calculateDarkHints(Bitmap source) { 375 if (source == null) { 376 return 0; 377 } 378 379 int[] pixels = new int[source.getWidth() * source.getHeight()]; 380 double totalLuminance = 0; 381 final int maxDarkPixels = (int) (pixels.length * MAX_DARK_AREA); 382 int darkPixels = 0; 383 source.getPixels(pixels, 0 /* offset */, source.getWidth(), 0 /* x */, 0 /* y */, 384 source.getWidth(), source.getHeight()); 385 386 // This bitmap was already resized to fit the maximum allowed area. 387 // Let's just loop through the pixels, no sweat! 388 float[] tmpHsl = new float[3]; 389 for (int i = 0; i < pixels.length; i++) { 390 ColorUtils.colorToHSL(pixels[i], tmpHsl); 391 final float luminance = tmpHsl[2]; 392 final int alpha = Color.alpha(pixels[i]); 393 // Make sure we don't have a dark pixel mass that will 394 // make text illegible. 395 final boolean satisfiesTextContrast = ContrastColorUtil 396 .calculateContrast(pixels[i], Color.BLACK) > DARK_PIXEL_CONTRAST; 397 if (!satisfiesTextContrast && alpha != 0) { 398 darkPixels++; 399 if (DEBUG_DARK_PIXELS) { 400 pixels[i] = Color.RED; 401 } 402 } 403 totalLuminance += luminance; 404 } 405 406 int hints = 0; 407 double meanLuminance = totalLuminance / pixels.length; 408 if (meanLuminance > BRIGHT_IMAGE_MEAN_LUMINANCE && darkPixels < maxDarkPixels) { 409 hints |= HINT_SUPPORTS_DARK_TEXT; 410 } 411 if (meanLuminance < DARK_THEME_MEAN_LUMINANCE) { 412 hints |= HINT_SUPPORTS_DARK_THEME; 413 } 414 415 if (DEBUG_DARK_PIXELS) { 416 try (FileOutputStream out = new FileOutputStream("/data/pixels.png")) { 417 source.setPixels(pixels, 0, source.getWidth(), 0, 0, source.getWidth(), 418 source.getHeight()); 419 source.compress(Bitmap.CompressFormat.PNG, 100, out); 420 } catch (Exception e) { 421 e.printStackTrace(); 422 } 423 Log.d("WallpaperColors", "l: " + meanLuminance + ", d: " + darkPixels + 424 " maxD: " + maxDarkPixels + " numPixels: " + pixels.length); 425 } 426 427 return hints; 428 } 429 calculateOptimalSize(int width, int height)430 private static Size calculateOptimalSize(int width, int height) { 431 // Calculate how big the bitmap needs to be. 432 // This avoids unnecessary processing and allocation inside Palette. 433 final int requestedArea = width * height; 434 double scale = 1; 435 if (requestedArea > MAX_WALLPAPER_EXTRACTION_AREA) { 436 scale = Math.sqrt(MAX_WALLPAPER_EXTRACTION_AREA / (double) requestedArea); 437 } 438 int newWidth = (int) (width * scale); 439 int newHeight = (int) (height * scale); 440 // Dealing with edge cases of the drawable being too wide or too tall. 441 // Width or height would end up being 0, in this case we'll set it to 1. 442 if (newWidth == 0) { 443 newWidth = 1; 444 } 445 if (newHeight == 0) { 446 newHeight = 1; 447 } 448 449 return new Size(newWidth, newHeight); 450 } 451 452 @Override toString()453 public String toString() { 454 final StringBuilder colors = new StringBuilder(); 455 for (int i = 0; i < mMainColors.size(); i++) { 456 colors.append(Integer.toHexString(mMainColors.get(i).toArgb())).append(" "); 457 } 458 return "[WallpaperColors: " + colors.toString() + "h: " + mColorHints + "]"; 459 } 460 } 461