Home | History | Annotate | Download | only in app
      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 
     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      */
    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      */
    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      */
    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      */
    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
    267     public int describeContents() {
    268         return 0;
    269     }
    270 
    271     @Override
    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      */
    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      */
    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      */
    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      */
    318     public @NonNull List<Color> getMainColors() {
    319         return Collections.unmodifiableList(mMainColors);
    320     }
    321 
    322     @Override
    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
    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      */
    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      */
    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      */
    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 
    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
    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