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