• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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