blob: a2864b9d83af431ba1bcbff77af23f6852f9fc30 [file] [log] [blame]
Lucas Dupinc40608c2017-04-14 18:33:08 -07001/*
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
17package android.app;
18
Lucas Dupin84b89d92017-05-09 12:16:19 -070019import android.annotation.NonNull;
20import android.annotation.Nullable;
21import android.graphics.Bitmap;
22import android.graphics.Canvas;
Lucas Dupinc40608c2017-04-14 18:33:08 -070023import android.graphics.Color;
Lucas Dupin84b89d92017-05-09 12:16:19 -070024import android.graphics.drawable.Drawable;
Lucas Dupinc40608c2017-04-14 18:33:08 -070025import android.os.Parcel;
26import android.os.Parcelable;
Lucas Dupin84b89d92017-05-09 12:16:19 -070027import android.util.Size;
Lucas Dupinc40608c2017-04-14 18:33:08 -070028
Lucas Dupin4bd24f32017-06-29 12:20:29 -070029import com.android.internal.graphics.ColorUtils;
Lucas Dupin84b89d92017-05-09 12:16:19 -070030import com.android.internal.graphics.palette.Palette;
Lucas Dupin1d3c00d52017-06-05 08:40:39 -070031import com.android.internal.graphics.palette.VariationalKMeansQuantizer;
Lucas Dupinc40608c2017-04-14 18:33:08 -070032
Lucas Dupinea1fb1e2017-04-05 17:39:44 -070033import java.util.ArrayList;
Lucas Dupin84b89d92017-05-09 12:16:19 -070034import java.util.Collections;
Lucas Dupinc40608c2017-04-14 18:33:08 -070035import java.util.List;
36
37/**
Lucas Dupin84b89d92017-05-09 12:16:19 -070038 * Provides information about the colors of a wallpaper.
39 * <p>
Lucas Dupinbdffdd52017-06-28 09:49:47 -070040 * Exposes the 3 most visually representative colors of a wallpaper. Can be either
Lucas Dupin84b89d92017-05-09 12:16:19 -070041 * {@link WallpaperColors#getPrimaryColor()}, {@link WallpaperColors#getSecondaryColor()}
42 * or {@link WallpaperColors#getTertiaryColor()}.
Lucas Dupinc40608c2017-04-14 18:33:08 -070043 */
44public final class WallpaperColors implements Parcelable {
45
Lucas Dupin84b89d92017-05-09 12:16:19 -070046 /**
47 * Specifies that dark text is preferred over the current wallpaper for best presentation.
48 * <p>
49 * eg. A launcher may set its text color to black if this flag is specified.
Lucas Dupinbdffdd52017-06-28 09:49:47 -070050 * @hide
Lucas Dupin84b89d92017-05-09 12:16:19 -070051 */
Lucas Dupine2efebc2017-08-11 10:30:58 -070052 public static final int HINT_SUPPORTS_DARK_TEXT = 1 << 0;
Lucas Dupin84b89d92017-05-09 12:16:19 -070053
Lucas Dupin4bd24f32017-06-29 12:20:29 -070054 /**
55 * Specifies that dark theme is preferred over the current wallpaper for best presentation.
56 * <p>
57 * eg. A launcher may set its drawer color to black if this flag is specified.
58 * @hide
59 */
Lucas Dupine2efebc2017-08-11 10:30:58 -070060 public static final int HINT_SUPPORTS_DARK_THEME = 1 << 1;
61
62 /**
63 * Specifies that this object was generated by extracting colors from a bitmap.
64 * @hide
65 */
66 public static final int HINT_FROM_BITMAP = 1 << 2;
Lucas Dupin4bd24f32017-06-29 12:20:29 -070067
Lucas Dupin84b89d92017-05-09 12:16:19 -070068 // Maximum size that a bitmap can have to keep our calculations sane
69 private static final int MAX_BITMAP_SIZE = 112;
70
71 // Even though we have a maximum size, we'll mainly match bitmap sizes
72 // using the area instead. This way our comparisons are aspect ratio independent.
73 private static final int MAX_WALLPAPER_EXTRACTION_AREA = MAX_BITMAP_SIZE * MAX_BITMAP_SIZE;
74
75 // When extracting the main colors, only consider colors
76 // present in at least MIN_COLOR_OCCURRENCE of the image
77 private static final float MIN_COLOR_OCCURRENCE = 0.05f;
78
Lucas Dupin4bd24f32017-06-29 12:20:29 -070079 // Decides when dark theme is optimal for this wallpaper
80 private static final float DARK_THEME_MEAN_LUMINANCE = 0.25f;
Lucas Dupin84b89d92017-05-09 12:16:19 -070081 // Minimum mean luminosity that an image needs to have to support dark text
Lucas Dupin4bd24f32017-06-29 12:20:29 -070082 private static final float BRIGHT_IMAGE_MEAN_LUMINANCE = 0.75f;
Lucas Dupin84b89d92017-05-09 12:16:19 -070083 // We also check if the image has dark pixels in it,
84 // to avoid bright images with some dark spots.
85 private static final float DARK_PIXEL_LUMINANCE = 0.45f;
86 private static final float MAX_DARK_AREA = 0.05f;
87
88 private final ArrayList<Color> mMainColors;
89 private int mColorHints;
Lucas Dupinea1fb1e2017-04-05 17:39:44 -070090
Lucas Dupinc40608c2017-04-14 18:33:08 -070091 public WallpaperColors(Parcel parcel) {
Lucas Dupin84b89d92017-05-09 12:16:19 -070092 mMainColors = new ArrayList<>();
93 final int count = parcel.readInt();
94 for (int i = 0; i < count; i++) {
95 final int colorInt = parcel.readInt();
96 Color color = Color.valueOf(colorInt);
97 mMainColors.add(color);
Lucas Dupinea1fb1e2017-04-05 17:39:44 -070098 }
Lucas Dupin84b89d92017-05-09 12:16:19 -070099 mColorHints = parcel.readInt();
Lucas Dupinc40608c2017-04-14 18:33:08 -0700100 }
101
102 /**
Lucas Dupin84b89d92017-05-09 12:16:19 -0700103 * Constructs {@link WallpaperColors} from a drawable.
104 * <p>
Lucas Dupinbdffdd52017-06-28 09:49:47 -0700105 * Main colors will be extracted from the drawable.
Lucas Dupinc40608c2017-04-14 18:33:08 -0700106 *
Lucas Dupin84b89d92017-05-09 12:16:19 -0700107 * @param drawable Source where to extract from.
Lucas Dupinc40608c2017-04-14 18:33:08 -0700108 */
Lucas Dupin84b89d92017-05-09 12:16:19 -0700109 public static WallpaperColors fromDrawable(Drawable drawable) {
110 int width = drawable.getIntrinsicWidth();
111 int height = drawable.getIntrinsicHeight();
112
113 // Some drawables do not have intrinsic dimensions
114 if (width <= 0 || height <= 0) {
115 width = MAX_BITMAP_SIZE;
116 height = MAX_BITMAP_SIZE;
117 }
118
119 Size optimalSize = calculateOptimalSize(width, height);
120 Bitmap bitmap = Bitmap.createBitmap(optimalSize.getWidth(), optimalSize.getHeight(),
121 Bitmap.Config.ARGB_8888);
122 final Canvas bmpCanvas = new Canvas(bitmap);
123 drawable.setBounds(0, 0, bitmap.getWidth(), bitmap.getHeight());
124 drawable.draw(bmpCanvas);
125
126 final WallpaperColors colors = WallpaperColors.fromBitmap(bitmap);
127 bitmap.recycle();
128
129 return colors;
Lucas Dupinc40608c2017-04-14 18:33:08 -0700130 }
131
132 /**
Lucas Dupin84b89d92017-05-09 12:16:19 -0700133 * Constructs {@link WallpaperColors} from a bitmap.
134 * <p>
Lucas Dupinbdffdd52017-06-28 09:49:47 -0700135 * Main colors will be extracted from the bitmap.
Lucas Dupinc40608c2017-04-14 18:33:08 -0700136 *
Lucas Dupin84b89d92017-05-09 12:16:19 -0700137 * @param bitmap Source where to extract from.
Lucas Dupinc40608c2017-04-14 18:33:08 -0700138 */
Lucas Dupin84b89d92017-05-09 12:16:19 -0700139 public static WallpaperColors fromBitmap(@NonNull Bitmap bitmap) {
140 if (bitmap == null) {
141 throw new IllegalArgumentException("Bitmap can't be null");
142 }
143
144 final int bitmapArea = bitmap.getWidth() * bitmap.getHeight();
Lucas Dupin42acf602017-07-13 16:32:44 -0700145 boolean shouldRecycle = false;
Lucas Dupin84b89d92017-05-09 12:16:19 -0700146 if (bitmapArea > MAX_WALLPAPER_EXTRACTION_AREA) {
Lucas Dupin42acf602017-07-13 16:32:44 -0700147 shouldRecycle = true;
Lucas Dupin84b89d92017-05-09 12:16:19 -0700148 Size optimalSize = calculateOptimalSize(bitmap.getWidth(), bitmap.getHeight());
Lucas Dupin42acf602017-07-13 16:32:44 -0700149 bitmap = Bitmap.createScaledBitmap(bitmap, optimalSize.getWidth(),
Lucas Dupin84b89d92017-05-09 12:16:19 -0700150 optimalSize.getHeight(), true /* filter */);
Lucas Dupin84b89d92017-05-09 12:16:19 -0700151 }
152
153 final Palette palette = Palette
154 .from(bitmap)
Lucas Dupin1d3c00d52017-06-05 08:40:39 -0700155 .setQuantizer(new VariationalKMeansQuantizer())
156 .maximumColorCount(5)
Lucas Dupin84b89d92017-05-09 12:16:19 -0700157 .clearFilters()
158 .resizeBitmapArea(MAX_WALLPAPER_EXTRACTION_AREA)
159 .generate();
160
161 // Remove insignificant colors and sort swatches by population
162 final ArrayList<Palette.Swatch> swatches = new ArrayList<>(palette.getSwatches());
163 final float minColorArea = bitmap.getWidth() * bitmap.getHeight() * MIN_COLOR_OCCURRENCE;
164 swatches.removeIf(s -> s.getPopulation() < minColorArea);
165 swatches.sort((a, b) -> b.getPopulation() - a.getPopulation());
166
167 final int swatchesSize = swatches.size();
168 Color primary = null, secondary = null, tertiary = null;
169
170 swatchLoop:
171 for (int i = 0; i < swatchesSize; i++) {
172 Color color = Color.valueOf(swatches.get(i).getRgb());
173 switch (i) {
174 case 0:
175 primary = color;
176 break;
177 case 1:
178 secondary = color;
179 break;
180 case 2:
181 tertiary = color;
182 break;
183 default:
184 // out of bounds
185 break swatchLoop;
186 }
187 }
188
Lucas Dupinb5e50532018-05-24 16:33:14 +0000189 int hints = calculateDarkHints(bitmap);
Lucas Dupin42acf602017-07-13 16:32:44 -0700190
191 if (shouldRecycle) {
192 bitmap.recycle();
193 }
194
Lucas Dupine2efebc2017-08-11 10:30:58 -0700195 return new WallpaperColors(primary, secondary, tertiary, HINT_FROM_BITMAP | hints);
Lucas Dupin84b89d92017-05-09 12:16:19 -0700196 }
197
198 /**
Lucas Dupinbdffdd52017-06-28 09:49:47 -0700199 * Constructs a new object from three colors.
200 *
201 * @param primaryColor Primary color.
202 * @param secondaryColor Secondary color.
203 * @param tertiaryColor Tertiary color.
204 * @see WallpaperColors#fromBitmap(Bitmap)
205 * @see WallpaperColors#fromDrawable(Drawable)
206 */
207 public WallpaperColors(@NonNull Color primaryColor, @Nullable Color secondaryColor,
208 @Nullable Color tertiaryColor) {
209 this(primaryColor, secondaryColor, tertiaryColor, 0);
210 }
211
212 /**
Lucas Dupin84b89d92017-05-09 12:16:19 -0700213 * Constructs a new object from three colors, where hints can be specified.
214 *
215 * @param primaryColor Primary color.
216 * @param secondaryColor Secondary color.
217 * @param tertiaryColor Tertiary color.
218 * @param colorHints A combination of WallpaperColor hints.
219 * @see WallpaperColors#HINT_SUPPORTS_DARK_TEXT
220 * @see WallpaperColors#fromBitmap(Bitmap)
221 * @see WallpaperColors#fromDrawable(Drawable)
Lucas Dupinbdffdd52017-06-28 09:49:47 -0700222 * @hide
Lucas Dupin84b89d92017-05-09 12:16:19 -0700223 */
224 public WallpaperColors(@NonNull Color primaryColor, @Nullable Color secondaryColor,
225 @Nullable Color tertiaryColor, int colorHints) {
226
227 if (primaryColor == null) {
228 throw new IllegalArgumentException("Primary color should never be null.");
229 }
230
231 mMainColors = new ArrayList<>(3);
232 mMainColors.add(primaryColor);
233 if (secondaryColor != null) {
234 mMainColors.add(secondaryColor);
235 }
236 if (tertiaryColor != null) {
237 if (secondaryColor == null) {
238 throw new IllegalArgumentException("tertiaryColor can't be specified when "
239 + "secondaryColor is null");
240 }
241 mMainColors.add(tertiaryColor);
242 }
243
244 mColorHints = colorHints;
Lucas Dupinc40608c2017-04-14 18:33:08 -0700245 }
246
247 public static final Creator<WallpaperColors> CREATOR = new Creator<WallpaperColors>() {
248 @Override
249 public WallpaperColors createFromParcel(Parcel in) {
250 return new WallpaperColors(in);
251 }
252
253 @Override
254 public WallpaperColors[] newArray(int size) {
255 return new WallpaperColors[size];
256 }
257 };
258
259 @Override
260 public int describeContents() {
261 return 0;
262 }
263
264 @Override
265 public void writeToParcel(Parcel dest, int flags) {
Lucas Dupin84b89d92017-05-09 12:16:19 -0700266 List<Color> mainColors = getMainColors();
267 int count = mainColors.size();
Lucas Dupinea1fb1e2017-04-05 17:39:44 -0700268 dest.writeInt(count);
Lucas Dupin84b89d92017-05-09 12:16:19 -0700269 for (int i = 0; i < count; i++) {
270 Color color = mainColors.get(i);
271 dest.writeInt(color.toArgb());
Lucas Dupinea1fb1e2017-04-05 17:39:44 -0700272 }
Lucas Dupin84b89d92017-05-09 12:16:19 -0700273 dest.writeInt(mColorHints);
Lucas Dupinc40608c2017-04-14 18:33:08 -0700274 }
275
276 /**
Lucas Dupin84b89d92017-05-09 12:16:19 -0700277 * Gets the most visually representative color of the wallpaper.
278 * "Visually representative" means easily noticeable in the image,
279 * probably happening at high frequency.
280 *
281 * @return A color.
Lucas Dupinc40608c2017-04-14 18:33:08 -0700282 */
Lucas Dupin84b89d92017-05-09 12:16:19 -0700283 public @NonNull Color getPrimaryColor() {
284 return mMainColors.get(0);
285 }
286
287 /**
288 * Gets the second most preeminent color of the wallpaper. Can be null.
289 *
290 * @return A color, may be null.
291 */
292 public @Nullable Color getSecondaryColor() {
293 return mMainColors.size() < 2 ? null : mMainColors.get(1);
294 }
295
296 /**
297 * Gets the third most preeminent color of the wallpaper. Can be null.
298 *
299 * @return A color, may be null.
300 */
301 public @Nullable Color getTertiaryColor() {
302 return mMainColors.size() < 3 ? null : mMainColors.get(2);
303 }
304
305 /**
306 * List of most preeminent colors, sorted by importance.
307 *
308 * @return List of colors.
309 * @hide
310 */
311 public @NonNull List<Color> getMainColors() {
312 return Collections.unmodifiableList(mMainColors);
Lucas Dupinea1fb1e2017-04-05 17:39:44 -0700313 }
314
315 @Override
316 public boolean equals(Object o) {
317 if (o == null || getClass() != o.getClass()) {
318 return false;
319 }
320
321 WallpaperColors other = (WallpaperColors) o;
Lucas Dupin84b89d92017-05-09 12:16:19 -0700322 return mMainColors.equals(other.mMainColors)
323 && mColorHints == other.mColorHints;
Lucas Dupinea1fb1e2017-04-05 17:39:44 -0700324 }
325
326 @Override
327 public int hashCode() {
Lucas Dupin84b89d92017-05-09 12:16:19 -0700328 return 31 * mMainColors.hashCode() + mColorHints;
Lucas Dupinc40608c2017-04-14 18:33:08 -0700329 }
330
331 /**
Lucas Dupin84b89d92017-05-09 12:16:19 -0700332 * Combination of WallpaperColor hints.
Lucas Dupinc40608c2017-04-14 18:33:08 -0700333 *
Lucas Dupin84b89d92017-05-09 12:16:19 -0700334 * @see WallpaperColors#HINT_SUPPORTS_DARK_TEXT
335 * @return True if dark text is supported.
Lucas Dupinbdffdd52017-06-28 09:49:47 -0700336 * @hide
Lucas Dupinc40608c2017-04-14 18:33:08 -0700337 */
Lucas Dupin84b89d92017-05-09 12:16:19 -0700338 public int getColorHints() {
339 return mColorHints;
Lucas Dupinea1fb1e2017-04-05 17:39:44 -0700340 }
341
Lucas Dupin84b89d92017-05-09 12:16:19 -0700342 /**
343 * @param colorHints Combination of WallpaperColors hints.
344 * @see WallpaperColors#HINT_SUPPORTS_DARK_TEXT
345 * @hide
346 */
347 public void setColorHints(int colorHints) {
348 mColorHints = colorHints;
349 }
350
351 /**
352 * Checks if image is bright and clean enough to support light text.
353 *
354 * @param source What to read.
355 * @return Whether image supports dark text or not.
356 */
Lucas Dupine2efebc2017-08-11 10:30:58 -0700357 private static int calculateDarkHints(Bitmap source) {
Lucas Dupin84b89d92017-05-09 12:16:19 -0700358 if (source == null) {
Lucas Dupin4bd24f32017-06-29 12:20:29 -0700359 return 0;
Lucas Dupinea1fb1e2017-04-05 17:39:44 -0700360 }
361
Lucas Dupin84b89d92017-05-09 12:16:19 -0700362 int[] pixels = new int[source.getWidth() * source.getHeight()];
363 double totalLuminance = 0;
364 final int maxDarkPixels = (int) (pixels.length * MAX_DARK_AREA);
365 int darkPixels = 0;
366 source.getPixels(pixels, 0 /* offset */, source.getWidth(), 0 /* x */, 0 /* y */,
367 source.getWidth(), source.getHeight());
Lucas Dupinea1fb1e2017-04-05 17:39:44 -0700368
Lucas Dupin84b89d92017-05-09 12:16:19 -0700369 // This bitmap was already resized to fit the maximum allowed area.
370 // Let's just loop through the pixels, no sweat!
Lucas Dupin4bd24f32017-06-29 12:20:29 -0700371 float[] tmpHsl = new float[3];
Lucas Dupin84b89d92017-05-09 12:16:19 -0700372 for (int i = 0; i < pixels.length; i++) {
Lucas Dupin4bd24f32017-06-29 12:20:29 -0700373 ColorUtils.colorToHSL(pixels[i], tmpHsl);
374 final float luminance = tmpHsl[2];
Lucas Dupin84b89d92017-05-09 12:16:19 -0700375 final int alpha = Color.alpha(pixels[i]);
Lucas Dupin84b89d92017-05-09 12:16:19 -0700376 // Make sure we don't have a dark pixel mass that will
377 // make text illegible.
378 if (luminance < DARK_PIXEL_LUMINANCE && alpha != 0) {
379 darkPixels++;
Lucas Dupinea1fb1e2017-04-05 17:39:44 -0700380 }
Lucas Dupin84b89d92017-05-09 12:16:19 -0700381 totalLuminance += luminance;
Lucas Dupinea1fb1e2017-04-05 17:39:44 -0700382 }
Lucas Dupin4bd24f32017-06-29 12:20:29 -0700383
384 int hints = 0;
385 double meanLuminance = totalLuminance / pixels.length;
386 if (meanLuminance > BRIGHT_IMAGE_MEAN_LUMINANCE && darkPixels < maxDarkPixels) {
387 hints |= HINT_SUPPORTS_DARK_TEXT;
388 }
389 if (meanLuminance < DARK_THEME_MEAN_LUMINANCE) {
390 hints |= HINT_SUPPORTS_DARK_THEME;
391 }
392
393 return hints;
Lucas Dupin84b89d92017-05-09 12:16:19 -0700394 }
395
396 private static Size calculateOptimalSize(int width, int height) {
397 // Calculate how big the bitmap needs to be.
398 // This avoids unnecessary processing and allocation inside Palette.
399 final int requestedArea = width * height;
400 double scale = 1;
401 if (requestedArea > MAX_WALLPAPER_EXTRACTION_AREA) {
402 scale = Math.sqrt(MAX_WALLPAPER_EXTRACTION_AREA / (double) requestedArea);
403 }
404 int newWidth = (int) (width * scale);
405 int newHeight = (int) (height * scale);
406 // Dealing with edge cases of the drawable being too wide or too tall.
407 // Width or height would end up being 0, in this case we'll set it to 1.
408 if (newWidth == 0) {
409 newWidth = 1;
410 }
411 if (newHeight == 0) {
412 newHeight = 1;
413 }
414
415 return new Size(newWidth, newHeight);
Lucas Dupinc40608c2017-04-14 18:33:08 -0700416 }
Lucas Dupin50ba9912017-07-14 11:55:05 -0700417
418 @Override
419 public String toString() {
420 final StringBuilder colors = new StringBuilder();
421 for (int i = 0; i < mMainColors.size(); i++) {
422 colors.append(Integer.toHexString(mMainColors.get(i).toArgb())).append(" ");
423 }
424 return "[WallpaperColors: " + colors.toString() + "h: " + mColorHints + "]";
425 }
Lucas Dupinc40608c2017-04-14 18:33:08 -0700426}