Add monet to AOSP

Test: manual
Test: atest ColorSchemeTest
Test: atest SystemPalette
Fixes: 199401230
Change-Id: I0695257765335f12ad47d5d8f40d0885c652bff6
This commit is contained in:
Lucas Dupin
2021-08-31 00:19:39 +00:00
parent e7c3d62316
commit 6844741fb8
10 changed files with 519 additions and 23 deletions

View File

@@ -74,6 +74,11 @@ final class DisabledWallpaperManager extends WallpaperManager {
return false;
}
private static int unsupportedInt() {
if (DEBUG) Log.w(TAG, "unsupported method called; returning -1", new Exception());
return -1;
}
@Override
public Drawable getDrawable() {
return unsupported();
@@ -189,12 +194,12 @@ final class DisabledWallpaperManager extends WallpaperManager {
@Override
public int getWallpaperId(int which) {
return unsupported();
return unsupportedInt();
}
@Override
public int getWallpaperIdForUser(int which, int userId) {
return unsupported();
return unsupportedInt();
}
@Override
@@ -209,7 +214,8 @@ final class DisabledWallpaperManager extends WallpaperManager {
@Override
public int setResource(int resid, int which) throws IOException {
return unsupported();
unsupported();
return 0;
}
@Override
@@ -220,19 +226,22 @@ final class DisabledWallpaperManager extends WallpaperManager {
@Override
public int setBitmap(Bitmap fullImage, Rect visibleCropHint, boolean allowBackup)
throws IOException {
return unsupported();
unsupported();
return 0;
}
@Override
public int setBitmap(Bitmap fullImage, Rect visibleCropHint, boolean allowBackup, int which)
throws IOException {
return unsupported();
unsupported();
return 0;
}
@Override
public int setBitmap(Bitmap fullImage, Rect visibleCropHint, boolean allowBackup, int which,
int userId) throws IOException {
return unsupported();
unsupported();
return 0;
}
@Override
@@ -243,13 +252,15 @@ final class DisabledWallpaperManager extends WallpaperManager {
@Override
public int setStream(InputStream bitmapData, Rect visibleCropHint, boolean allowBackup)
throws IOException {
return unsupported();
unsupported();
return 0;
}
@Override
public int setStream(InputStream bitmapData, Rect visibleCropHint, boolean allowBackup,
int which) throws IOException {
return unsupported();
unsupported();
return 0;
}
@Override
@@ -259,12 +270,12 @@ final class DisabledWallpaperManager extends WallpaperManager {
@Override
public int getDesiredMinimumWidth() {
return unsupported();
return unsupportedInt();
}
@Override
public int getDesiredMinimumHeight() {
return unsupported();
return unsupportedInt();
}
@Override

View File

@@ -92,6 +92,7 @@ android_library {
"iconloader_base",
"SystemUI-tags",
"SystemUI-proto",
"monet",
"dagger2",
"jsr330",
"lottie",
@@ -179,6 +180,7 @@ android_library {
"mockito-target-extended-minus-junit4",
"testables",
"truth-prebuilt",
"monet",
"dagger2",
"jsr330",
"WindowManager-Shell",

View File

@@ -0,0 +1,31 @@
//
// Copyright (C) 2021 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
package {
default_applicable_licenses: ["Android-Apache-2.0"],
}
java_library {
name: "monet",
platform_apis: true,
static_libs: [
"androidx.annotation_annotation",
"androidx.core_core",
],
srcs: [
"src/**/*.java",
"src/**/*.kt",
],
}

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2021 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.systemui.monet">
</manifest>

View File

@@ -0,0 +1,246 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.monet
import android.annotation.ColorInt
import android.app.WallpaperColors
import android.graphics.Color
import com.android.internal.graphics.ColorUtils
import com.android.internal.graphics.cam.Cam
import com.android.internal.graphics.cam.CamUtils.lstarFromInt
import kotlin.math.absoluteValue
import kotlin.math.roundToInt
const val TAG = "ColorScheme"
const val ACCENT1_CHROMA = 48.0f
const val ACCENT2_CHROMA = 16.0f
const val ACCENT3_CHROMA = 32.0f
const val ACCENT3_HUE_SHIFT = 60.0f
const val NEUTRAL1_CHROMA = 4.0f
const val NEUTRAL2_CHROMA = 8.0f
const val GOOGLE_BLUE = 0xFF1b6ef3.toInt()
const val MIN_CHROMA = 15
const val MIN_LSTAR = 10
public class ColorScheme(@ColorInt seed: Int, val darkTheme: Boolean) {
val accent1: List<Int>
val accent2: List<Int>
val accent3: List<Int>
val neutral1: List<Int>
val neutral2: List<Int>
constructor(wallpaperColors: WallpaperColors, darkTheme: Boolean):
this(getSeedColor(wallpaperColors), darkTheme)
val allAccentColors: List<Int>
get() {
val allColors = mutableListOf<Int>()
allColors.addAll(accent1)
allColors.addAll(accent2)
allColors.addAll(accent3)
return allColors
}
val allNeutralColors: List<Int>
get() {
val allColors = mutableListOf<Int>()
allColors.addAll(neutral1)
allColors.addAll(neutral2)
return allColors
}
val backgroundColor
get() = ColorUtils.setAlphaComponent(if (darkTheme) neutral1[8] else neutral1[0], 0xFF)
val accentColor
get() = ColorUtils.setAlphaComponent(if (darkTheme) accent1[2] else accent1[6], 0xFF)
init {
val seedArgb = if (seed == Color.TRANSPARENT) GOOGLE_BLUE else seed
val camSeed = Cam.fromInt(seedArgb)
val hue = camSeed.hue
val chroma = camSeed.chroma.coerceAtLeast(ACCENT1_CHROMA)
accent1 = Shades.of(hue, chroma).toList()
accent2 = Shades.of(hue, ACCENT2_CHROMA).toList()
accent3 = Shades.of(hue + ACCENT3_HUE_SHIFT, ACCENT3_CHROMA).toList()
neutral1 = Shades.of(hue, NEUTRAL1_CHROMA).toList()
neutral2 = Shades.of(hue, NEUTRAL2_CHROMA).toList()
}
override fun toString(): String {
return "ColorScheme {\n" +
" neutral1: ${humanReadable(neutral1)}\n" +
" neutral2: ${humanReadable(neutral2)}\n" +
" accent1: ${humanReadable(accent1)}\n" +
" accent2: ${humanReadable(accent2)}\n" +
" accent3: ${humanReadable(accent3)}\n" +
"}"
}
companion object {
/**
* Identifies a color to create a color scheme from.
*
* @param wallpaperColors Colors extracted from an image via quantization.
* @return ARGB int representing the color
*/
@JvmStatic
@ColorInt
fun getSeedColor(wallpaperColors: WallpaperColors): Int {
return getSeedColors(wallpaperColors).first()
}
/**
* Filters and ranks colors from WallpaperColors.
*
* @param wallpaperColors Colors extracted from an image via quantization.
* @return List of ARGB ints, ordered from highest scoring to lowest.
*/
@JvmStatic
fun getSeedColors(wallpaperColors: WallpaperColors): List<Int> {
val totalPopulation = wallpaperColors.allColors.values.reduce { a, b -> a + b }
.toDouble()
val totalPopulationMeaningless = (totalPopulation == 0.0)
if (totalPopulationMeaningless) {
// WallpaperColors with a population of 0 indicate the colors didn't come from
// quantization. Instead of scoring, trust the ordering of the provided primary
// secondary/tertiary colors.
//
// In this case, the colors are usually from a Live Wallpaper.
val distinctColors = wallpaperColors.mainColors.map {
it.toArgb()
}.distinct().filter {
val cam = Cam.fromInt(it)
val lstar = lstarFromInt(it)
cam.chroma >= MIN_CHROMA && lstar >= MIN_LSTAR
}.toList()
if (distinctColors.isEmpty()) {
return listOf(GOOGLE_BLUE)
}
return distinctColors
}
val intToProportion = wallpaperColors.allColors.mapValues {
it.value.toDouble() / totalPopulation
}
val intToCam = wallpaperColors.allColors.mapValues { Cam.fromInt(it.key) }
// Get an array with 360 slots. A slot contains the percentage of colors with that hue.
val hueProportions = huePopulations(intToCam, intToProportion)
// Map each color to the percentage of the image with its hue.
val intToHueProportion = wallpaperColors.allColors.mapValues {
val cam = intToCam[it.key]!!
val hue = cam.hue.roundToInt()
var proportion = 0.0
for (i in hue - 15..hue + 15) {
proportion += hueProportions[wrapDegrees(i)]
}
proportion
}
// Remove any inappropriate seed colors. For example, low chroma colors look grayscale
// raising their chroma will turn them to a much louder color that may not have been
// in the image.
val filteredIntToCam = intToCam.filter {
val cam = it.value
val lstar = lstarFromInt(it.key)
val proportion = intToHueProportion[it.key]!!
cam.chroma >= MIN_CHROMA && lstar >= MIN_LSTAR &&
(totalPopulationMeaningless || proportion > 0.01)
}
// Sort the colors by score, from high to low.
val seeds = mutableListOf<Int>()
val intToScoreIntermediate = filteredIntToCam.mapValues {
score(it.value, intToHueProportion[it.key]!!)
}
val intToScore = intToScoreIntermediate.entries.toMutableList()
intToScore.sortByDescending { it.value }
// Go through the colors, from high score to low score. If there isn't already a seed
// color with a hue close to color being examined, add the color being examined to the
// seed colors.
for (entry in intToScore) {
val int = entry.key
val existingSeedNearby = seeds.find {
val hueA = intToCam[int]!!.hue
val hueB = intToCam[it]!!.hue
hueDiff(hueA, hueB) < 15 } != null
if (existingSeedNearby) {
continue
}
seeds.add(int)
}
if (seeds.isEmpty()) {
// Use gBlue 500 if there are 0 colors
seeds.add(GOOGLE_BLUE)
}
return seeds
}
private fun wrapDegrees(degrees: Int): Int {
return when {
degrees < 0 -> {
(degrees % 360) + 360
}
degrees >= 360 -> {
degrees % 360
}
else -> {
degrees
}
}
}
private fun hueDiff(a: Float, b: Float): Float {
return 180f - ((a - b).absoluteValue - 180f).absoluteValue
}
private fun humanReadable(colors: List<Int>): String {
return colors.joinToString { "#" + Integer.toHexString(it) }
}
private fun score(cam: Cam, proportion: Double): Double {
val proportionScore = 0.7 * 100.0 * proportion
val chromaScore = if (cam.chroma < ACCENT1_CHROMA) 0.1 * (cam.chroma - ACCENT1_CHROMA)
else 0.3 * (cam.chroma - ACCENT1_CHROMA)
return chromaScore + proportionScore
}
private fun huePopulations(
camByColor: Map<Int, Cam>,
populationByColor: Map<Int, Double>
): List<Double> {
val huePopulation = List(size = 360, init = { 0.0 }).toMutableList()
for (entry in populationByColor.entries) {
val population = populationByColor[entry.key]!!
val cam = camByColor[entry.key]!!
val hue = cam.hue.roundToInt() % 360
huePopulation[hue] = huePopulation[hue] + population
}
return huePopulation
}
}
}

View File

@@ -0,0 +1,64 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.monet;
import androidx.annotation.ColorInt;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.graphics.ColorUtils;
/**
* Generate sets of colors that are shades of the same color
*/
@VisibleForTesting
public class Shades {
/**
* Combining the ability to convert between relative luminance and perceptual luminance with
* contrast leads to a design system that can be based on a linear value to determine contrast,
* rather than a ratio.
*
* This codebase implements a design system that has that property, and as a result, we can
* guarantee that any shades 5 steps from each other have a contrast ratio of at least 4.5.
* 4.5 is the requirement for smaller text contrast in WCAG 2.1 and earlier.
*
* However, lstar 50 does _not_ have a contrast ratio >= 4.5 with lstar 100.
* lstar 49.6 is the smallest lstar that will lead to a contrast ratio >= 4.5 with lstar 100,
* and it also contrasts >= 4.5 with lstar 100.
*/
public static final float MIDDLE_LSTAR = 49.6f;
/**
* Generate shades of a color. Ordered in lightness _descending_.
* <p>
* The first shade will be at 95% lightness, the next at 90, 80, etc. through 0.
*
* @param hue hue in CAM16 color space
* @param chroma chroma in CAM16 color space
* @return shades of a color, as argb integers. Ordered by lightness descending.
*/
public static @ColorInt int[] of(float hue, float chroma) {
int[] shades = new int[12];
shades[0] = ColorUtils.CAMToColor(hue, chroma, 99);
shades[1] = ColorUtils.CAMToColor(hue, chroma, 95);
for (int i = 2; i < 12; i++) {
float lStar = (i == 6) ? MIDDLE_LSTAR : 100 - 10 * (i - 1);
shades[i] = ColorUtils.CAMToColor(hue, chroma, lStar);
}
return shades;
}
}

View File

@@ -22,7 +22,7 @@
<bool name="flag_notification_pipeline2_rendering">false</bool>
<bool name="flag_notif_updates">true</bool>
<bool name="flag_monet">false</bool>
<bool name="flag_monet">true</bool>
<!-- AOD/Lockscreen alternate layout -->
<bool name="flag_keyguard_layout">true</bool>

View File

@@ -37,6 +37,7 @@ import android.content.IntentFilter;
import android.content.om.FabricatedOverlay;
import android.content.om.OverlayIdentifier;
import android.content.pm.UserInfo;
import android.content.res.Configuration;
import android.database.ContentObserver;
import android.graphics.Color;
import android.net.Uri;
@@ -47,9 +48,11 @@ import android.provider.Settings;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.Log;
import android.util.TypedValue;
import androidx.annotation.NonNull;
import com.android.internal.graphics.ColorUtils;
import com.android.systemui.Dumpable;
import com.android.systemui.SystemUI;
import com.android.systemui.broadcast.BroadcastDispatcher;
@@ -59,6 +62,7 @@ import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.keyguard.WakefulnessLifecycle;
import com.android.systemui.monet.ColorScheme;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.policy.DeviceProvisionedController;
import com.android.systemui.statusbar.policy.DeviceProvisionedController.DeviceProvisionedListener;
@@ -71,6 +75,7 @@ import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Executor;
@@ -107,6 +112,7 @@ public class ThemeOverlayController extends SystemUI implements Dumpable {
private DeviceProvisionedController mDeviceProvisionedController;
private WallpaperColors mCurrentColors;
private WallpaperManager mWallpaperManager;
private ColorScheme mColorScheme;
// If fabricated overlays were already created for the current theme.
private boolean mNeedsOverlayCreation;
// Dominant color extracted from wallpaper, NOT the color used on the overlay
@@ -403,25 +409,47 @@ public class ThemeOverlayController extends SystemUI implements Dumpable {
* Return the main theme color from a given {@link WallpaperColors} instance.
*/
protected int getNeutralColor(@NonNull WallpaperColors wallpaperColors) {
return wallpaperColors.getPrimaryColor().toArgb();
return ColorScheme.getSeedColor(wallpaperColors);
}
protected int getAccentColor(@NonNull WallpaperColors wallpaperColors) {
Color accentCandidate = wallpaperColors.getSecondaryColor();
if (accentCandidate == null) {
accentCandidate = wallpaperColors.getTertiaryColor();
}
if (accentCandidate == null) {
accentCandidate = wallpaperColors.getPrimaryColor();
}
return accentCandidate.toArgb();
return ColorScheme.getSeedColor(wallpaperColors);
}
/**
* Given a color candidate, return an overlay definition.
*/
protected @Nullable FabricatedOverlay getOverlay(int color, int type) {
return null;
boolean nightMode = (mContext.getResources().getConfiguration().uiMode
& Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES;
mColorScheme = new ColorScheme(color, nightMode);
List<Integer> colorShades = type == ACCENT
? mColorScheme.getAllAccentColors() : mColorScheme.getAllNeutralColors();
String name = type == ACCENT ? "accent" : "neutral";
int paletteSize = mColorScheme.getAccent1().size();
FabricatedOverlay.Builder overlay =
new FabricatedOverlay.Builder("com.android.systemui", name, "android");
for (int i = 0; i < colorShades.size(); i++) {
int luminosity = i % paletteSize;
int paletteIndex = i / paletteSize + 1;
String resourceName;
switch (luminosity) {
case 0:
resourceName = "android:color/system_" + name + paletteIndex + "_10";
break;
case 1:
resourceName = "android:color/system_" + name + paletteIndex + "_50";
break;
default:
int l = luminosity - 1;
resourceName = "android:color/system_" + name + paletteIndex + "_" + l + "00";
}
overlay.setResourceValue(resourceName, TypedValue.TYPE_INT_COLOR_ARGB8,
ColorUtils.setAlphaComponent(colorShades.get(i), 0xFF));
}
return overlay.build();
}
private void updateThemeOverlays() {
@@ -521,7 +549,7 @@ public class ThemeOverlayController extends SystemUI implements Dumpable {
.map(key -> key + " -> " + categoryToPackage.get(key)).collect(
Collectors.joining(", ")));
}
Runnable overlaysAppliedRunnable = () -> onOverlaysApplied();
Runnable overlaysAppliedRunnable = this::onOverlaysApplied;
if (mNeedsOverlayCreation) {
mNeedsOverlayCreation = false;
mThemeManager.applyCurrentUserOverlays(categoryToPackage, new FabricatedOverlay[] {
@@ -544,6 +572,7 @@ public class ThemeOverlayController extends SystemUI implements Dumpable {
pw.println("mSecondaryOverlay=" + mSecondaryOverlay);
pw.println("mNeutralOverlay=" + mNeutralOverlay);
pw.println("mIsMonetEnabled=" + mIsMonetEnabled);
pw.println("mColorScheme=" + mColorScheme);
pw.println("mNeedsOverlayCreation=" + mNeedsOverlayCreation);
pw.println("mAcceptColorEvents=" + mAcceptColorEvents);
pw.println("mDeferredThemeEvaluation=" + mDeferredThemeEvaluation);

View File

@@ -0,0 +1,93 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.monet;
import android.app.WallpaperColors;
import android.graphics.Color;
import android.testing.AndroidTestingRunner;
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.List;
@SmallTest
@RunWith(AndroidTestingRunner.class)
public class ColorSchemeTest extends SysuiTestCase {
@Test
public void testFilterTransparency() {
ColorScheme colorScheme = new ColorScheme(Color.TRANSPARENT, false /* darkTheme */);
Assert.assertEquals(colorScheme.getAllAccentColors(),
new ColorScheme(0xFF1b6ef3, false).getAllAccentColors());
}
@Test
public void testDontFilterOpaque() {
ColorScheme colorScheme = new ColorScheme(0xFFFF0000, false /* darkTheme */);
Assert.assertNotEquals(colorScheme.getAllAccentColors(),
new ColorScheme(0xFF1b6ef3, false).getAllAccentColors());
}
@Test
public void testUniqueColors() {
WallpaperColors wallpaperColors = new WallpaperColors(Color.valueOf(0xffaec00a),
Color.valueOf(0xffaec00a), Color.valueOf(0xffaec00a));
List<Integer> rankedSeedColors = ColorScheme.getSeedColors(wallpaperColors);
Assert.assertEquals(rankedSeedColors, List.of(0xffaec00a));
}
@Test
public void testFiltersInvalidColors() {
WallpaperColors wallpaperColors = new WallpaperColors(Color.valueOf(0xff5e7ea2),
Color.valueOf(0xff5e7ea2), Color.valueOf(0xff000000));
List<Integer> rankedSeedColors = ColorScheme.getSeedColors(wallpaperColors);
Assert.assertEquals(rankedSeedColors, List.of(0xff5e7ea2));
}
@Test
public void testInvalidColorBecomesGBlue() {
WallpaperColors wallpaperColors = new WallpaperColors(Color.valueOf(0xff000000), null,
null);
List<Integer> rankedSeedColors = ColorScheme.getSeedColors(wallpaperColors);
Assert.assertEquals(rankedSeedColors, List.of(0xFF1b6ef3));
}
@Test
public void testDontFilterRRGGBB() {
ColorScheme colorScheme = new ColorScheme(0xFF0000, false /* darkTheme */);
Assert.assertEquals(colorScheme.getAllAccentColors(),
new ColorScheme(0xFFFF0000, false).getAllAccentColors());
}
@Test
public void testNoPopulationSignal() {
WallpaperColors wallpaperColors = new WallpaperColors(Color.valueOf(0xffaec00a),
Color.valueOf(0xffbe0000), Color.valueOf(0xffcc040f));
List<Integer> rankedSeedColors = ColorScheme.getSeedColors(wallpaperColors);
Assert.assertEquals(rankedSeedColors, List.of(0xffaec00a, 0xffbe0000, 0xffcc040f));
}
}

View File

@@ -167,7 +167,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase {
assertThat(themeOverlays.getValue().get(OVERLAY_CATEGORY_SYSTEM_PALETTE))
.isEqualTo(new OverlayIdentifier("ffff0000"));
assertThat(themeOverlays.getValue().get(OVERLAY_CATEGORY_ACCENT_COLOR))
.isEqualTo(new OverlayIdentifier("ff0000ff"));
.isEqualTo(new OverlayIdentifier("ffff0000"));
// Should not ask again if changed to same value
mColorsListener.getValue().onColorsChanged(mainColors, WallpaperManager.FLAG_SYSTEM);