1 /*
2  * Copyright (C) 2024 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 com.android.systemui.monet;
18 
19 import android.annotation.ColorInt;
20 import android.app.WallpaperColors;
21 import android.graphics.Color;
22 
23 import com.android.internal.graphics.ColorUtils;
24 
25 import com.google.ux.material.libmonet.dynamiccolor.DynamicScheme;
26 import com.google.ux.material.libmonet.hct.Hct;
27 import com.google.ux.material.libmonet.scheme.SchemeContent;
28 import com.google.ux.material.libmonet.scheme.SchemeExpressive;
29 import com.google.ux.material.libmonet.scheme.SchemeFruitSalad;
30 import com.google.ux.material.libmonet.scheme.SchemeMonochrome;
31 import com.google.ux.material.libmonet.scheme.SchemeNeutral;
32 import com.google.ux.material.libmonet.scheme.SchemeRainbow;
33 import com.google.ux.material.libmonet.scheme.SchemeTonalSpot;
34 import com.google.ux.material.libmonet.scheme.SchemeVibrant;
35 
36 import java.util.AbstractMap;
37 import java.util.ArrayList;
38 import java.util.Collections;
39 import java.util.List;
40 import java.util.Map;
41 import java.util.stream.Collectors;
42 
43 /**
44  * @deprecated Please use com.google.ux.material.libmonet.dynamiccolor.MaterialDynamicColors instead
45  */
46 @Deprecated
47 public class ColorScheme {
48     public static final int GOOGLE_BLUE = 0xFF1b6ef3;
49     private static final float ACCENT1_CHROMA = 48.0f;
50     private static final int MIN_CHROMA = 5;
51 
52     @ColorInt
53     private final int mSeed;
54     private final boolean mIsDark;
55     private final Style mStyle;
56     private final DynamicScheme mMaterialScheme;
57     private final TonalPalette mAccent1;
58     private final TonalPalette mAccent2;
59     private final TonalPalette mAccent3;
60     private final TonalPalette mNeutral1;
61     private final TonalPalette mNeutral2;
62     private final Hct mProposedSeedHct;
63 
64 
ColorScheme(@olorInt int seed, boolean isDark, Style style, double contrastLevel)65     public ColorScheme(@ColorInt int seed, boolean isDark, Style style, double contrastLevel) {
66         this.mSeed = seed;
67         this.mIsDark = isDark;
68         this.mStyle = style;
69 
70         mProposedSeedHct = Hct.fromInt(seed);
71         Hct seedHct = Hct.fromInt(
72                 seed == Color.TRANSPARENT
73                         ? GOOGLE_BLUE
74                         : (style != Style.CONTENT
75                                 && mProposedSeedHct.getChroma() < 5
76                                 ? GOOGLE_BLUE
77                                 : seed));
78 
79         mMaterialScheme = switch (style) {
80             case SPRITZ -> new SchemeNeutral(seedHct, isDark, contrastLevel);
81             case TONAL_SPOT -> new SchemeTonalSpot(seedHct, isDark, contrastLevel);
82             case VIBRANT -> new SchemeVibrant(seedHct, isDark, contrastLevel);
83             case EXPRESSIVE -> new SchemeExpressive(seedHct, isDark, contrastLevel);
84             case RAINBOW -> new SchemeRainbow(seedHct, isDark, contrastLevel);
85             case FRUIT_SALAD -> new SchemeFruitSalad(seedHct, isDark, contrastLevel);
86             case CONTENT -> new SchemeContent(seedHct, isDark, contrastLevel);
87             case MONOCHROMATIC -> new SchemeMonochrome(seedHct, isDark, contrastLevel);
88             // SystemUI Schemes
89             case CLOCK -> new SchemeClock(seedHct, isDark, contrastLevel);
90             case CLOCK_VIBRANT -> new SchemeClockVibrant(seedHct, isDark, contrastLevel);
91             default -> throw new IllegalArgumentException("Unknown style: " + style);
92         };
93 
94         mAccent1 = new TonalPalette(mMaterialScheme.primaryPalette);
95         mAccent2 = new TonalPalette(mMaterialScheme.secondaryPalette);
96         mAccent3 = new TonalPalette(mMaterialScheme.tertiaryPalette);
97         mNeutral1 = new TonalPalette(mMaterialScheme.neutralPalette);
98         mNeutral2 = new TonalPalette(mMaterialScheme.neutralVariantPalette);
99     }
100 
ColorScheme(@olorInt int seed, boolean darkTheme)101     public ColorScheme(@ColorInt int seed, boolean darkTheme) {
102         this(seed, darkTheme, Style.TONAL_SPOT);
103     }
104 
ColorScheme(@olorInt int seed, boolean darkTheme, Style style)105     public ColorScheme(@ColorInt int seed, boolean darkTheme, Style style) {
106         this(seed, darkTheme, style, 0.0);
107     }
108 
ColorScheme(WallpaperColors wallpaperColors, boolean darkTheme, Style style)109     public ColorScheme(WallpaperColors wallpaperColors, boolean darkTheme, Style style) {
110         this(getSeedColor(wallpaperColors, style != Style.CONTENT), darkTheme, style);
111     }
112 
ColorScheme(WallpaperColors wallpaperColors, boolean darkTheme)113     public ColorScheme(WallpaperColors wallpaperColors, boolean darkTheme) {
114         this(wallpaperColors, darkTheme, Style.TONAL_SPOT);
115     }
116 
getBackgroundColor()117     public int getBackgroundColor() {
118         return ColorUtils.setAlphaComponent(mIsDark
119                 ? mNeutral1.getS700()
120                 : mNeutral1.getS10(), 0xFF);
121     }
122 
getAccentColor()123     public int getAccentColor() {
124         return ColorUtils.setAlphaComponent(mIsDark
125                 ? mAccent1.getS100()
126                 : mAccent1.getS500(), 0xFF);
127     }
128 
getSeedTone()129     public double getSeedTone() {
130         return 1000d - mProposedSeedHct.getTone() * 10d;
131     }
132 
getSeed()133     public int getSeed() {
134         return mSeed;
135     }
136 
getStyle()137     public Style getStyle() {
138         return mStyle;
139     }
140 
getMaterialScheme()141     public DynamicScheme getMaterialScheme() {
142         return mMaterialScheme;
143     }
144 
getAccent1()145     public TonalPalette getAccent1() {
146         return mAccent1;
147     }
148 
getAccent2()149     public TonalPalette getAccent2() {
150         return mAccent2;
151     }
152 
getAccent3()153     public TonalPalette getAccent3() {
154         return mAccent3;
155     }
156 
getNeutral1()157     public TonalPalette getNeutral1() {
158         return mNeutral1;
159     }
160 
getNeutral2()161     public TonalPalette getNeutral2() {
162         return mNeutral2;
163     }
164 
165     @Override
toString()166     public String toString() {
167         return "ColorScheme {\n"
168                 + "  seed color: " + stringForColor(mSeed) + "\n"
169                 + "  style: " + mStyle + "\n"
170                 + "  palettes: \n"
171                 + "  " + humanReadable("PRIMARY", mAccent1.allShades) + "\n"
172                 + "  " + humanReadable("SECONDARY", mAccent2.allShades) + "\n"
173                 + "  " + humanReadable("TERTIARY", mAccent3.allShades) + "\n"
174                 + "  " + humanReadable("NEUTRAL", mNeutral1.allShades) + "\n"
175                 + "  " + humanReadable("NEUTRAL VARIANT", mNeutral2.allShades) + "\n"
176                 + "}";
177     }
178 
179     /**
180      * Identifies a color to create a color scheme from.
181      *
182      * @param wallpaperColors Colors extracted from an image via quantization.
183      * @param filter          If false, allow colors that have low chroma, creating grayscale
184      *                        themes.
185      * @return ARGB int representing the color
186      */
187     @ColorInt
getSeedColor(WallpaperColors wallpaperColors, boolean filter)188     public static int getSeedColor(WallpaperColors wallpaperColors, boolean filter) {
189         return getSeedColors(wallpaperColors, filter).get(0);
190     }
191 
192     /**
193      * Identifies a color to create a color scheme from. Defaults Filter to TRUE
194      *
195      * @param wallpaperColors Colors extracted from an image via quantization.
196      * @return ARGB int representing the color
197      */
getSeedColor(WallpaperColors wallpaperColors)198     public static int getSeedColor(WallpaperColors wallpaperColors) {
199         return getSeedColor(wallpaperColors, true);
200     }
201 
202     /**
203      * Filters and ranks colors from WallpaperColors.
204      *
205      * @param wallpaperColors Colors extracted from an image via quantization.
206      * @param filter          If false, allow colors that have low chroma, creating grayscale
207      *                        themes.
208      * @return List of ARGB ints, ordered from highest scoring to lowest.
209      */
getSeedColors(WallpaperColors wallpaperColors, boolean filter)210     public static List<Integer> getSeedColors(WallpaperColors wallpaperColors, boolean filter) {
211         double totalPopulation = wallpaperColors.getAllColors().values().stream().mapToInt(
212                 Integer::intValue).sum();
213         boolean totalPopulationMeaningless = (totalPopulation == 0.0);
214 
215         if (totalPopulationMeaningless) {
216             // WallpaperColors with a population of 0 indicate the colors didn't come from
217             // quantization. Instead of scoring, trust the ordering of the provided primary
218             // secondary/tertiary colors.
219             //
220             // In this case, the colors are usually from a Live Wallpaper.
221             List<Integer> distinctColors = wallpaperColors.getMainColors().stream()
222                     .map(Color::toArgb)
223                     .distinct()
224                     .filter(color -> !filter || Hct.fromInt(color).getChroma() >= MIN_CHROMA)
225                     .collect(Collectors.toList());
226             if (distinctColors.isEmpty()) {
227                 return List.of(GOOGLE_BLUE);
228             }
229             return distinctColors;
230         }
231 
232         Map<Integer, Double> intToProportion = wallpaperColors.getAllColors().entrySet().stream()
233                 .collect(Collectors.toMap(Map.Entry::getKey,
234                         entry -> entry.getValue().doubleValue() / totalPopulation));
235         Map<Integer, Hct> intToHct = wallpaperColors.getAllColors().entrySet().stream()
236                 .collect(Collectors.toMap(Map.Entry::getKey, entry -> Hct.fromInt(entry.getKey())));
237 
238         // Get an array with 360 slots. A slot contains the percentage of colors with that hue.
239         List<Double> hueProportions = huePopulations(intToHct, intToProportion, filter);
240         // Map each color to the percentage of the image with its hue.
241         Map<Integer, Double> intToHueProportion = wallpaperColors.getAllColors().entrySet().stream()
242                 .collect(Collectors.toMap(Map.Entry::getKey, entry -> {
243                     Hct hct = intToHct.get(entry.getKey());
244                     int hue = (int) Math.round(hct.getHue());
245                     double proportion = 0.0;
246                     for (int i = hue - 15; i <= hue + 15; i++) {
247                         proportion += hueProportions.get(wrapDegrees(i));
248                     }
249                     return proportion;
250                 }));
251         // Remove any inappropriate seed colors. For example, low chroma colors look grayscale
252         // raising their chroma will turn them to a much louder color that may not have been
253         // in the image.
254         Map<Integer, Hct> filteredIntToHct = filter
255                 ? intToHct
256                     .entrySet()
257                     .stream()
258                     .filter(entry -> {
259                         Hct hct = entry.getValue();
260                         double proportion = intToHueProportion.get(entry.getKey());
261                         return hct.getChroma() >= MIN_CHROMA && proportion > 0.01;
262                     })
263                     .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))
264                 : intToHct;
265         // Sort the colors by score, from high to low.
266         List<Map.Entry<Integer, Double>> intToScore = filteredIntToHct.entrySet().stream()
267                 .map(entry -> new AbstractMap.SimpleEntry<>(entry.getKey(),
268                         score(entry.getValue(), intToHueProportion.get(entry.getKey()))))
269                 .sorted(Map.Entry.<Integer, Double>comparingByValue().reversed())
270                 .collect(Collectors.toList());
271 
272         // Go through the colors, from high score to low score.
273         // If the color is distinct in hue from colors picked so far, pick the color.
274         // Iteratively decrease the amount of hue distinctness required, thus ensuring we
275         // maximize difference between colors.
276         int minimumHueDistance = 15;
277         List<Integer> seeds = new ArrayList<>();
278         for (int i = 90; i >= minimumHueDistance; i--) {
279             seeds.clear();
280             for (Map.Entry<Integer, Double> entry : intToScore) {
281                 int currentColor = entry.getKey();
282                 int finalI = i;
283                 boolean existingSeedNearby = seeds.stream().anyMatch(seed -> {
284                     double hueA = intToHct.get(currentColor).getHue();
285                     double hueB = intToHct.get(seed).getHue();
286                     return hueDiff(hueA, hueB) < finalI;
287                 });
288                 if (existingSeedNearby) {
289                     continue;
290                 }
291                 seeds.add(currentColor);
292                 if (seeds.size() >= 4) {
293                     break;
294                 }
295             }
296             if (!seeds.isEmpty()) {
297                 break;
298             }
299         }
300 
301         if (seeds.isEmpty()) {
302             // Use gBlue 500 if there are 0 colors
303             seeds.add(GOOGLE_BLUE);
304         }
305 
306         return seeds;
307     }
308 
309     /**
310      * Filters and ranks colors from WallpaperColors. Defaults Filter to TRUE
311      *
312      * @param newWallpaperColors Colors extracted from an image via quantization.
313      *                        themes.
314      * @return List of ARGB ints, ordered from highest scoring to lowest.
315      */
getSeedColors(WallpaperColors newWallpaperColors)316     public static List<Integer> getSeedColors(WallpaperColors newWallpaperColors) {
317         return getSeedColors(newWallpaperColors, true);
318     }
319 
wrapDegrees(int degrees)320     private static int wrapDegrees(int degrees) {
321         if (degrees < 0) {
322             return (degrees % 360) + 360;
323         } else if (degrees >= 360) {
324             return degrees % 360;
325         } else {
326             return degrees;
327         }
328     }
329 
hueDiff(double a, double b)330     private static double hueDiff(double a, double b) {
331         return 180f - (Math.abs(a - b) - 180f);
332     }
333 
stringForColor(int color)334     private static String stringForColor(int color) {
335         int width = 4;
336         Hct hct = Hct.fromInt(color);
337         String h = "H" + String.format("%" + width + "s", Math.round(hct.getHue()));
338         String c = "C" + String.format("%" + width + "s", Math.round(hct.getChroma()));
339         String t = "T" + String.format("%" + width + "s", Math.round(hct.getTone()));
340         String hex = Integer.toHexString(color & 0xffffff).toUpperCase();
341         return h + c + t + " = #" + hex;
342     }
343 
humanReadable(String paletteName, List<Integer> colors)344     private static String humanReadable(String paletteName, List<Integer> colors) {
345         return paletteName + "\n"
346                 + colors
347                     .stream()
348                     .map(ColorScheme::stringForColor)
349                     .collect(Collectors.joining("\n"));
350     }
351 
score(Hct hct, double proportion)352     private static double score(Hct hct, double proportion) {
353         double proportionScore = 0.7 * 100.0 * proportion;
354         double chromaScore = hct.getChroma() < ACCENT1_CHROMA
355                 ? 0.1 * (hct.getChroma() - ACCENT1_CHROMA)
356                 : 0.3 * (hct.getChroma() - ACCENT1_CHROMA);
357         return chromaScore + proportionScore;
358     }
359 
360     private static List<Double> huePopulations(Map<Integer, Hct> hctByColor,
361             Map<Integer, Double> populationByColor, boolean filter) {
362         List<Double> huePopulation = new ArrayList<>(Collections.nCopies(360, 0.0));
363 
364         for (Map.Entry<Integer, Double> entry : populationByColor.entrySet()) {
365             double population = entry.getValue();
366             Hct hct = hctByColor.get(entry.getKey());
367             int hue = (int) Math.round(hct.getHue()) % 360;
368             if (filter && hct.getChroma() <= MIN_CHROMA) {
369                 continue;
370             }
371             huePopulation.set(hue, huePopulation.get(hue) + population);
372         }
373 
374         return huePopulation;
375     }
376 }
377