• 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.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 
WallpaperColors(Parcel parcel)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      */
fromDrawable(Drawable drawable)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      */
fromBitmap(@onNull Bitmap bitmap)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      */
WallpaperColors(@onNull Color primaryColor, @Nullable Color secondaryColor, @Nullable Color tertiaryColor)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      */
WallpaperColors(@onNull Color primaryColor, @Nullable Color secondaryColor, @Nullable Color tertiaryColor, int colorHints)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
describeContents()267     public int describeContents() {
268         return 0;
269     }
270 
271     @Override
writeToParcel(Parcel dest, int flags)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      */
getPrimaryColor()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      */
getSecondaryColor()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      */
getTertiaryColor()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      */
getMainColors()318     public @NonNull List<Color> getMainColors() {
319         return Collections.unmodifiableList(mMainColors);
320     }
321 
322     @Override
equals(Object o)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
hashCode()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      */
getColorHints()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      */
setColorHints(int colorHints)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      */
calculateDarkHints(Bitmap source)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 
calculateOptimalSize(int width, int height)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
toString()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