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.FloatRange; 20 import android.annotation.IntDef; 21 import android.annotation.NonNull; 22 import android.annotation.Nullable; 23 import android.graphics.Bitmap; 24 import android.graphics.Canvas; 25 import android.graphics.Color; 26 import android.graphics.Rect; 27 import android.graphics.drawable.Drawable; 28 import android.os.Parcel; 29 import android.os.Parcelable; 30 import android.os.SystemProperties; 31 import android.util.Log; 32 import android.util.MathUtils; 33 import android.util.Size; 34 35 import com.android.internal.graphics.ColorUtils; 36 import com.android.internal.graphics.cam.Cam; 37 import com.android.internal.graphics.palette.CelebiQuantizer; 38 import com.android.internal.graphics.palette.Palette; 39 import com.android.internal.graphics.palette.VariationalKMeansQuantizer; 40 import com.android.internal.util.ContrastColorUtil; 41 42 import java.io.FileOutputStream; 43 import java.lang.annotation.Retention; 44 import java.lang.annotation.RetentionPolicy; 45 import java.util.ArrayList; 46 import java.util.Collections; 47 import java.util.HashMap; 48 import java.util.List; 49 import java.util.Map; 50 import java.util.Objects; 51 import java.util.Set; 52 53 /** 54 * Provides information about the colors of a wallpaper. 55 * <p> 56 * Exposes the 3 most visually representative colors of a wallpaper. Can be either 57 * {@link WallpaperColors#getPrimaryColor()}, {@link WallpaperColors#getSecondaryColor()} 58 * or {@link WallpaperColors#getTertiaryColor()}. 59 */ 60 public final class WallpaperColors implements Parcelable { 61 /** 62 * @hide 63 */ 64 @IntDef(prefix = "HINT_", value = {HINT_SUPPORTS_DARK_TEXT, HINT_SUPPORTS_DARK_THEME}, 65 flag = true) 66 @Retention(RetentionPolicy.SOURCE) 67 public @interface ColorsHints {} 68 69 private static final boolean DEBUG_DARK_PIXELS = false; 70 71 /** 72 * Specifies that dark text is preferred over the current wallpaper for best presentation. 73 * <p> 74 * eg. A launcher may set its text color to black if this flag is specified. 75 */ 76 public static final int HINT_SUPPORTS_DARK_TEXT = 1 << 0; 77 78 /** 79 * Specifies that dark theme is preferred over the current wallpaper for best presentation. 80 * <p> 81 * eg. A launcher may set its drawer color to black if this flag is specified. 82 */ 83 public static final int HINT_SUPPORTS_DARK_THEME = 1 << 1; 84 85 /** 86 * Specifies that this object was generated by extracting colors from a bitmap. 87 * @hide 88 */ 89 public static final int HINT_FROM_BITMAP = 1 << 2; 90 91 // Maximum size that a bitmap can have to keep our calculations valid 92 private static final int MAX_BITMAP_SIZE = 112; 93 94 // Even though we have a maximum size, we'll mainly match bitmap sizes 95 // using the area instead. This way our comparisons are aspect ratio independent. 96 private static final int MAX_WALLPAPER_EXTRACTION_AREA = MAX_BITMAP_SIZE * MAX_BITMAP_SIZE; 97 98 // When extracting the main colors, only consider colors 99 // present in at least MIN_COLOR_OCCURRENCE of the image 100 private static final float MIN_COLOR_OCCURRENCE = 0.05f; 101 102 // Decides when dark theme is optimal for this wallpaper 103 private static final float DARK_THEME_MEAN_LUMINANCE = 0.3f; 104 // Minimum mean luminosity that an image needs to have to support dark text 105 private static final float BRIGHT_IMAGE_MEAN_LUMINANCE = SystemProperties.getInt( 106 "persist.wallpapercolors.threshold", 70) / 100f; 107 // We also check if the image has dark pixels in it, 108 // to avoid bright images with some dark spots. 109 private static final float DARK_PIXEL_CONTRAST = 5.5f; 110 private static final float MAX_DARK_AREA = SystemProperties.getInt( 111 "persist.wallpapercolors.max_dark_area", 5) / 100f; 112 113 private final List<Color> mMainColors; 114 private final Map<Integer, Integer> mAllColors; 115 private int mColorHints; 116 WallpaperColors(Parcel parcel)117 public WallpaperColors(Parcel parcel) { 118 mMainColors = new ArrayList<>(); 119 mAllColors = new HashMap<>(); 120 int count = parcel.readInt(); 121 for (int i = 0; i < count; i++) { 122 final int colorInt = parcel.readInt(); 123 Color color = Color.valueOf(colorInt); 124 mMainColors.add(color); 125 } 126 count = parcel.readInt(); 127 for (int i = 0; i < count; i++) { 128 final int colorInt = parcel.readInt(); 129 final int population = parcel.readInt(); 130 mAllColors.put(colorInt, population); 131 } 132 mColorHints = parcel.readInt(); 133 } 134 135 /** 136 * Constructs {@link WallpaperColors} from a drawable. 137 * <p> 138 * Main colors will be extracted from the drawable. 139 * 140 * @param drawable Source where to extract from. 141 */ fromDrawable(Drawable drawable)142 public static WallpaperColors fromDrawable(Drawable drawable) { 143 if (drawable == null) { 144 throw new IllegalArgumentException("Drawable cannot be null"); 145 } 146 147 Rect initialBounds = drawable.copyBounds(); 148 int width = drawable.getIntrinsicWidth(); 149 int height = drawable.getIntrinsicHeight(); 150 151 // Some drawables do not have intrinsic dimensions 152 if (width <= 0 || height <= 0) { 153 width = MAX_BITMAP_SIZE; 154 height = MAX_BITMAP_SIZE; 155 } 156 157 Size optimalSize = calculateOptimalSize(width, height); 158 Bitmap bitmap = Bitmap.createBitmap(optimalSize.getWidth(), optimalSize.getHeight(), 159 Bitmap.Config.ARGB_8888); 160 final Canvas bmpCanvas = new Canvas(bitmap); 161 drawable.setBounds(0, 0, bitmap.getWidth(), bitmap.getHeight()); 162 drawable.draw(bmpCanvas); 163 164 final WallpaperColors colors = WallpaperColors.fromBitmap(bitmap); 165 bitmap.recycle(); 166 167 drawable.setBounds(initialBounds); 168 return colors; 169 } 170 171 /** 172 * Constructs {@link WallpaperColors} from a bitmap. 173 * <p> 174 * Main colors will be extracted from the bitmap. 175 * 176 * @param bitmap Source where to extract from. 177 */ fromBitmap(@onNull Bitmap bitmap)178 public static WallpaperColors fromBitmap(@NonNull Bitmap bitmap) { 179 if (bitmap == null) { 180 throw new IllegalArgumentException("Bitmap can't be null"); 181 } 182 return fromBitmap(bitmap, 0f /* dimAmount */); 183 } 184 185 /** 186 * Constructs {@link WallpaperColors} from a bitmap with dimming applied. 187 * <p> 188 * Main colors will be extracted from the bitmap with dimming taken into account when 189 * calculating dark hints. 190 * 191 * @param bitmap Source where to extract from. 192 * @param dimAmount Wallpaper dim amount 193 * @hide 194 */ fromBitmap(@onNull Bitmap bitmap, @FloatRange (from = 0f, to = 1f) float dimAmount)195 public static WallpaperColors fromBitmap(@NonNull Bitmap bitmap, 196 @FloatRange (from = 0f, to = 1f) float dimAmount) { 197 Objects.requireNonNull(bitmap, "Bitmap can't be null"); 198 199 final int bitmapArea = bitmap.getWidth() * bitmap.getHeight(); 200 boolean shouldRecycle = false; 201 if (bitmapArea > MAX_WALLPAPER_EXTRACTION_AREA) { 202 shouldRecycle = true; 203 Size optimalSize = calculateOptimalSize(bitmap.getWidth(), bitmap.getHeight()); 204 bitmap = Bitmap.createScaledBitmap(bitmap, optimalSize.getWidth(), 205 optimalSize.getHeight(), false /* filter */); 206 } 207 208 final Palette palette; 209 if (ActivityManager.isLowRamDeviceStatic()) { 210 palette = Palette 211 .from(bitmap, new VariationalKMeansQuantizer()) 212 .maximumColorCount(5) 213 .resizeBitmapArea(MAX_WALLPAPER_EXTRACTION_AREA) 214 .generate(); 215 } else { 216 palette = Palette 217 .from(bitmap, new CelebiQuantizer()) 218 .maximumColorCount(128) 219 .resizeBitmapArea(MAX_WALLPAPER_EXTRACTION_AREA) 220 .generate(); 221 } 222 // Remove insignificant colors and sort swatches by population 223 final ArrayList<Palette.Swatch> swatches = new ArrayList<>(palette.getSwatches()); 224 swatches.sort((a, b) -> b.getPopulation() - a.getPopulation()); 225 226 final int swatchesSize = swatches.size(); 227 228 final Map<Integer, Integer> populationByColor = new HashMap<>(); 229 for (int i = 0; i < swatchesSize; i++) { 230 Palette.Swatch swatch = swatches.get(i); 231 int colorInt = swatch.getInt(); 232 populationByColor.put(colorInt, swatch.getPopulation()); 233 234 } 235 236 int hints = calculateDarkHints(bitmap, dimAmount); 237 238 if (shouldRecycle) { 239 bitmap.recycle(); 240 } 241 242 return new WallpaperColors(populationByColor, HINT_FROM_BITMAP | hints); 243 } 244 245 /** 246 * Constructs a new object from three colors. 247 * 248 * @param primaryColor Primary color. 249 * @param secondaryColor Secondary color. 250 * @param tertiaryColor Tertiary color. 251 * @see WallpaperColors#fromBitmap(Bitmap) 252 * @see WallpaperColors#fromDrawable(Drawable) 253 */ WallpaperColors(@onNull Color primaryColor, @Nullable Color secondaryColor, @Nullable Color tertiaryColor)254 public WallpaperColors(@NonNull Color primaryColor, @Nullable Color secondaryColor, 255 @Nullable Color tertiaryColor) { 256 this(primaryColor, secondaryColor, tertiaryColor, 0); 257 258 // Calculate dark theme support based on primary color. 259 final float[] tmpHsl = new float[3]; 260 ColorUtils.colorToHSL(primaryColor.toArgb(), tmpHsl); 261 final float luminance = tmpHsl[2]; 262 if (luminance < DARK_THEME_MEAN_LUMINANCE) { 263 mColorHints |= HINT_SUPPORTS_DARK_THEME; 264 } 265 } 266 267 /** 268 * Constructs a new object from three colors, where hints can be specified. 269 * 270 * @param primaryColor Primary color. 271 * @param secondaryColor Secondary color. 272 * @param tertiaryColor Tertiary color. 273 * @param colorHints A combination of color hints. 274 * @see WallpaperColors#fromBitmap(Bitmap) 275 * @see WallpaperColors#fromDrawable(Drawable) 276 */ WallpaperColors(@onNull Color primaryColor, @Nullable Color secondaryColor, @Nullable Color tertiaryColor, @ColorsHints int colorHints)277 public WallpaperColors(@NonNull Color primaryColor, @Nullable Color secondaryColor, 278 @Nullable Color tertiaryColor, @ColorsHints int colorHints) { 279 280 if (primaryColor == null) { 281 throw new IllegalArgumentException("Primary color should never be null."); 282 } 283 284 mMainColors = new ArrayList<>(3); 285 mAllColors = new HashMap<>(); 286 287 mMainColors.add(primaryColor); 288 mAllColors.put(primaryColor.toArgb(), 0); 289 if (secondaryColor != null) { 290 mMainColors.add(secondaryColor); 291 mAllColors.put(secondaryColor.toArgb(), 0); 292 } 293 if (tertiaryColor != null) { 294 if (secondaryColor == null) { 295 throw new IllegalArgumentException("tertiaryColor can't be specified when " 296 + "secondaryColor is null"); 297 } 298 mMainColors.add(tertiaryColor); 299 mAllColors.put(tertiaryColor.toArgb(), 0); 300 } 301 mColorHints = colorHints; 302 } 303 304 /** 305 * Constructs a new object from a set of colors, where hints can be specified. 306 * 307 * @param colorToPopulation Map with keys of colors, and value representing the number of 308 * occurrences of color in the wallpaper. 309 * @param colorHints A combination of color hints. 310 * @hide 311 * @see WallpaperColors#HINT_SUPPORTS_DARK_TEXT 312 * @see WallpaperColors#fromBitmap(Bitmap) 313 * @see WallpaperColors#fromDrawable(Drawable) 314 */ WallpaperColors(@onNull Map<Integer, Integer> colorToPopulation, @ColorsHints int colorHints)315 public WallpaperColors(@NonNull Map<Integer, Integer> colorToPopulation, 316 @ColorsHints int colorHints) { 317 mAllColors = colorToPopulation; 318 319 final Map<Integer, Cam> colorToCam = new HashMap<>(); 320 for (int color : colorToPopulation.keySet()) { 321 colorToCam.put(color, Cam.fromInt(color)); 322 } 323 final double[] hueProportions = hueProportions(colorToCam, colorToPopulation); 324 final Map<Integer, Double> colorToHueProportion = colorToHueProportion( 325 colorToPopulation.keySet(), colorToCam, hueProportions); 326 327 final Map<Integer, Double> colorToScore = new HashMap<>(); 328 for (Map.Entry<Integer, Double> mapEntry : colorToHueProportion.entrySet()) { 329 int color = mapEntry.getKey(); 330 double proportion = mapEntry.getValue(); 331 double score = score(colorToCam.get(color), proportion); 332 colorToScore.put(color, score); 333 } 334 ArrayList<Map.Entry<Integer, Double>> mapEntries = new ArrayList(colorToScore.entrySet()); 335 mapEntries.sort((a, b) -> b.getValue().compareTo(a.getValue())); 336 337 List<Integer> colorsByScoreDescending = new ArrayList<>(); 338 for (Map.Entry<Integer, Double> colorToScoreEntry : mapEntries) { 339 colorsByScoreDescending.add(colorToScoreEntry.getKey()); 340 } 341 342 List<Integer> mainColorInts = new ArrayList<>(); 343 findSeedColorLoop: 344 for (int color : colorsByScoreDescending) { 345 Cam cam = colorToCam.get(color); 346 for (int otherColor : mainColorInts) { 347 Cam otherCam = colorToCam.get(otherColor); 348 if (hueDiff(cam, otherCam) < 15) { 349 continue findSeedColorLoop; 350 } 351 } 352 mainColorInts.add(color); 353 } 354 List<Color> mainColors = new ArrayList<>(); 355 for (int colorInt : mainColorInts) { 356 mainColors.add(Color.valueOf(colorInt)); 357 } 358 mMainColors = mainColors; 359 mColorHints = colorHints; 360 } 361 hueDiff(Cam a, Cam b)362 private static double hueDiff(Cam a, Cam b) { 363 return (180f - Math.abs(Math.abs(a.getHue() - b.getHue()) - 180f)); 364 } 365 score(Cam cam, double proportion)366 private static double score(Cam cam, double proportion) { 367 return cam.getChroma() + (proportion * 100); 368 } 369 colorToHueProportion(Set<Integer> colors, Map<Integer, Cam> colorToCam, double[] hueProportions)370 private static Map<Integer, Double> colorToHueProportion(Set<Integer> colors, 371 Map<Integer, Cam> colorToCam, double[] hueProportions) { 372 Map<Integer, Double> colorToHueProportion = new HashMap<>(); 373 for (int color : colors) { 374 final int hue = wrapDegrees(Math.round(colorToCam.get(color).getHue())); 375 double proportion = 0.0; 376 for (int i = hue - 15; i < hue + 15; i++) { 377 proportion += hueProportions[wrapDegrees(i)]; 378 } 379 colorToHueProportion.put(color, proportion); 380 } 381 return colorToHueProportion; 382 } 383 wrapDegrees(int degrees)384 private static int wrapDegrees(int degrees) { 385 if (degrees < 0) { 386 return (degrees % 360) + 360; 387 } else if (degrees >= 360) { 388 return degrees % 360; 389 } else { 390 return degrees; 391 } 392 } 393 hueProportions(@onNull Map<Integer, Cam> colorToCam, Map<Integer, Integer> colorToPopulation)394 private static double[] hueProportions(@NonNull Map<Integer, Cam> colorToCam, 395 Map<Integer, Integer> colorToPopulation) { 396 final double[] proportions = new double[360]; 397 398 double totalPopulation = 0; 399 for (Map.Entry<Integer, Integer> entry : colorToPopulation.entrySet()) { 400 totalPopulation += entry.getValue(); 401 } 402 403 for (Map.Entry<Integer, Integer> entry : colorToPopulation.entrySet()) { 404 final int color = (int) entry.getKey(); 405 final int population = colorToPopulation.get(color); 406 final Cam cam = colorToCam.get(color); 407 final int hue = wrapDegrees(Math.round(cam.getHue())); 408 proportions[hue] = proportions[hue] + ((double) population / totalPopulation); 409 } 410 411 return proportions; 412 } 413 414 public static final @android.annotation.NonNull Creator<WallpaperColors> CREATOR = new Creator<WallpaperColors>() { 415 @Override 416 public WallpaperColors createFromParcel(Parcel in) { 417 return new WallpaperColors(in); 418 } 419 420 @Override 421 public WallpaperColors[] newArray(int size) { 422 return new WallpaperColors[size]; 423 } 424 }; 425 426 @Override describeContents()427 public int describeContents() { 428 return 0; 429 } 430 431 @Override writeToParcel(Parcel dest, int flags)432 public void writeToParcel(Parcel dest, int flags) { 433 List<Color> mainColors = getMainColors(); 434 int count = mainColors.size(); 435 dest.writeInt(count); 436 for (int i = 0; i < count; i++) { 437 Color color = mainColors.get(i); 438 dest.writeInt(color.toArgb()); 439 } 440 count = mAllColors.size(); 441 dest.writeInt(count); 442 for (Map.Entry<Integer, Integer> colorEntry : mAllColors.entrySet()) { 443 if (colorEntry.getKey() != null) { 444 dest.writeInt(colorEntry.getKey()); 445 Integer population = colorEntry.getValue(); 446 int populationInt = (population != null) ? population : 0; 447 dest.writeInt(populationInt); 448 } 449 } 450 dest.writeInt(mColorHints); 451 } 452 453 /** 454 * Gets the most visually representative color of the wallpaper. 455 * "Visually representative" means easily noticeable in the image, 456 * probably happening at high frequency. 457 * 458 * @return A color. 459 */ getPrimaryColor()460 public @NonNull Color getPrimaryColor() { 461 return mMainColors.get(0); 462 } 463 464 /** 465 * Gets the second most preeminent color of the wallpaper. Can be null. 466 * 467 * @return A color, may be null. 468 */ getSecondaryColor()469 public @Nullable Color getSecondaryColor() { 470 return mMainColors.size() < 2 ? null : mMainColors.get(1); 471 } 472 473 /** 474 * Gets the third most preeminent color of the wallpaper. Can be null. 475 * 476 * @return A color, may be null. 477 */ getTertiaryColor()478 public @Nullable Color getTertiaryColor() { 479 return mMainColors.size() < 3 ? null : mMainColors.get(2); 480 } 481 482 /** 483 * List of most preeminent colors, sorted by importance. 484 * 485 * @return List of colors. 486 * @hide 487 */ getMainColors()488 public @NonNull List<Color> getMainColors() { 489 return Collections.unmodifiableList(mMainColors); 490 } 491 492 /** 493 * Map of all colors. Key is rgb integer, value is importance of color. 494 * 495 * @return List of colors. 496 * @hide 497 */ getAllColors()498 public @NonNull Map<Integer, Integer> getAllColors() { 499 return Collections.unmodifiableMap(mAllColors); 500 } 501 502 503 @Override equals(@ullable Object o)504 public boolean equals(@Nullable Object o) { 505 if (o == null || getClass() != o.getClass()) { 506 return false; 507 } 508 509 WallpaperColors other = (WallpaperColors) o; 510 return mMainColors.equals(other.mMainColors) 511 && mAllColors.equals(other.mAllColors) 512 && mColorHints == other.mColorHints; 513 } 514 515 @Override hashCode()516 public int hashCode() { 517 return (31 * mMainColors.hashCode() * mAllColors.hashCode()) + mColorHints; 518 } 519 520 /** 521 * Returns the color hints for this instance. 522 * @return The color hints. 523 */ getColorHints()524 public @ColorsHints int getColorHints() { 525 return mColorHints; 526 } 527 528 /** 529 * Checks if image is bright and clean enough to support light text. 530 * 531 * @param source What to read. 532 * @param dimAmount How much wallpaper dim amount was applied. 533 * @return Whether image supports dark text or not. 534 */ calculateDarkHints(Bitmap source, float dimAmount)535 private static int calculateDarkHints(Bitmap source, float dimAmount) { 536 if (source == null) { 537 return 0; 538 } 539 540 dimAmount = MathUtils.saturate(dimAmount); 541 int[] pixels = new int[source.getWidth() * source.getHeight()]; 542 double totalLuminance = 0; 543 final int maxDarkPixels = (int) (pixels.length * MAX_DARK_AREA); 544 int darkPixels = 0; 545 source.getPixels(pixels, 0 /* offset */, source.getWidth(), 0 /* x */, 0 /* y */, 546 source.getWidth(), source.getHeight()); 547 548 // Create a new black layer with dimAmount as the alpha to be accounted for when computing 549 // the luminance. 550 int dimmingLayerAlpha = (int) (255 * dimAmount); 551 int blackTransparent = ColorUtils.setAlphaComponent(Color.BLACK, dimmingLayerAlpha); 552 553 // This bitmap was already resized to fit the maximum allowed area. 554 // Let's just loop through the pixels, no sweat! 555 float[] tmpHsl = new float[3]; 556 for (int i = 0; i < pixels.length; i++) { 557 int pixelColor = pixels[i]; 558 ColorUtils.colorToHSL(pixelColor, tmpHsl); 559 final int alpha = Color.alpha(pixelColor); 560 561 // Apply composite colors where the foreground is a black layer with an alpha value of 562 // the dim amount and the background is the wallpaper pixel color. 563 int compositeColors = ColorUtils.compositeColors(blackTransparent, pixelColor); 564 565 // Calculate the adjusted luminance of the dimmed wallpaper pixel color. 566 double adjustedLuminance = ColorUtils.calculateLuminance(compositeColors); 567 568 // Make sure we don't have a dark pixel mass that will 569 // make text illegible. 570 final boolean satisfiesTextContrast = ContrastColorUtil 571 .calculateContrast(pixelColor, Color.BLACK) > DARK_PIXEL_CONTRAST; 572 if (!satisfiesTextContrast && alpha != 0) { 573 darkPixels++; 574 if (DEBUG_DARK_PIXELS) { 575 pixels[i] = Color.RED; 576 } 577 } 578 totalLuminance += adjustedLuminance; 579 } 580 581 int hints = 0; 582 double meanLuminance = totalLuminance / pixels.length; 583 if (meanLuminance > BRIGHT_IMAGE_MEAN_LUMINANCE && darkPixels < maxDarkPixels) { 584 hints |= HINT_SUPPORTS_DARK_TEXT; 585 } 586 if (meanLuminance < DARK_THEME_MEAN_LUMINANCE) { 587 hints |= HINT_SUPPORTS_DARK_THEME; 588 } 589 590 if (DEBUG_DARK_PIXELS) { 591 try (FileOutputStream out = new FileOutputStream("/data/pixels.png")) { 592 source.setPixels(pixels, 0, source.getWidth(), 0, 0, source.getWidth(), 593 source.getHeight()); 594 source.compress(Bitmap.CompressFormat.PNG, 100, out); 595 } catch (Exception e) { 596 e.printStackTrace(); 597 } 598 Log.d("WallpaperColors", "l: " + meanLuminance + ", d: " + darkPixels + 599 " maxD: " + maxDarkPixels + " numPixels: " + pixels.length); 600 } 601 602 return hints; 603 } 604 calculateOptimalSize(int width, int height)605 private static Size calculateOptimalSize(int width, int height) { 606 // Calculate how big the bitmap needs to be. 607 // This avoids unnecessary processing and allocation inside Palette. 608 final int requestedArea = width * height; 609 double scale = 1; 610 if (requestedArea > MAX_WALLPAPER_EXTRACTION_AREA) { 611 scale = Math.sqrt(MAX_WALLPAPER_EXTRACTION_AREA / (double) requestedArea); 612 } 613 int newWidth = (int) (width * scale); 614 int newHeight = (int) (height * scale); 615 // Dealing with edge cases of the drawable being too wide or too tall. 616 // Width or height would end up being 0, in this case we'll set it to 1. 617 if (newWidth == 0) { 618 newWidth = 1; 619 } 620 if (newHeight == 0) { 621 newHeight = 1; 622 } 623 624 return new Size(newWidth, newHeight); 625 } 626 627 @Override toString()628 public String toString() { 629 final StringBuilder colors = new StringBuilder(); 630 for (int i = 0; i < mMainColors.size(); i++) { 631 colors.append(Integer.toHexString(mMainColors.get(i).toArgb())).append(" "); 632 } 633 return "[WallpaperColors: " + colors.toString() + "h: " + mColorHints + "]"; 634 } 635 } 636