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