Cleanup for SystemUI's ColorScheme
Bug: 327338148
Test: atest SystemPaletteTest
Test: atest ThemeOverlayControllerTest
Flag: NONE
Change-Id: Ic185d31d1206ea7eb5463ac80d09f27e02f0b35a
diff --git a/packages/SystemUI/monet/Android.bp b/packages/SystemUI/monet/Android.bp
index 98f7ace..c54fdab 100644
--- a/packages/SystemUI/monet/Android.bp
+++ b/packages/SystemUI/monet/Android.bp
@@ -24,6 +24,7 @@
static_libs: [
"androidx.annotation_annotation",
"androidx.core_core",
+ "libmonet",
],
srcs: [
"src/**/*.java",
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt b/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt
index 46a90f6..47a00f4 100644
--- a/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt
@@ -20,11 +20,17 @@
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
+import com.google.ux.material.libmonet.hct.Hct
+import com.google.ux.material.libmonet.scheme.DynamicScheme
+import com.google.ux.material.libmonet.scheme.SchemeContent
+import com.google.ux.material.libmonet.scheme.SchemeExpressive
+import com.google.ux.material.libmonet.scheme.SchemeFruitSalad
+import com.google.ux.material.libmonet.scheme.SchemeMonochrome
+import com.google.ux.material.libmonet.scheme.SchemeNeutral
+import com.google.ux.material.libmonet.scheme.SchemeRainbow
+import com.google.ux.material.libmonet.scheme.SchemeTonalSpot
+import com.google.ux.material.libmonet.scheme.SchemeVibrant
import kotlin.math.absoluteValue
-import kotlin.math.max
-import kotlin.math.min
import kotlin.math.roundToInt
const val TAG = "ColorScheme"
@@ -33,347 +39,65 @@
const val GOOGLE_BLUE = 0xFF1b6ef3.toInt()
const val MIN_CHROMA = 5
-internal interface Hue {
- fun get(sourceColor: Cam): Double
-
- /**
- * Given a hue, and a mapping of hues to hue rotations, find which hues in the mapping the hue
- * fall betweens, and use the hue rotation of the lower hue.
- *
- * @param sourceHue hue of source color
- * @param hueAndRotations list of pairs, where the first item in a pair is a hue, and the second
- * item in the pair is a hue rotation that should be applied
- */
- fun getHueRotation(sourceHue: Float, hueAndRotations: List<Pair<Int, Int>>): Double {
- val sanitizedSourceHue = (if (sourceHue < 0 || sourceHue >= 360) 0 else sourceHue).toFloat()
- for (i in 0..hueAndRotations.size - 2) {
- val thisHue = hueAndRotations[i].first.toFloat()
- val nextHue = hueAndRotations[i + 1].first.toFloat()
- if (thisHue <= sanitizedSourceHue && sanitizedSourceHue < nextHue) {
- return ColorScheme.wrapDegreesDouble(
- sanitizedSourceHue.toDouble() + hueAndRotations[i].second
- )
- }
- }
-
- // If this statement executes, something is wrong, there should have been a rotation
- // found using the arrays.
- return sourceHue.toDouble()
- }
-}
-
-internal class HueSource : Hue {
- override fun get(sourceColor: Cam): Double {
- return sourceColor.hue.toDouble()
- }
-}
-
-internal class HueAdd(val amountDegrees: Double) : Hue {
- override fun get(sourceColor: Cam): Double {
- return ColorScheme.wrapDegreesDouble(sourceColor.hue.toDouble() + amountDegrees)
- }
-}
-
-internal class HueSubtract(val amountDegrees: Double) : Hue {
- override fun get(sourceColor: Cam): Double {
- return ColorScheme.wrapDegreesDouble(sourceColor.hue.toDouble() - amountDegrees)
- }
-}
-
-internal class HueVibrantSecondary() : Hue {
- val hueToRotations =
- listOf(
- Pair(0, 18),
- Pair(41, 15),
- Pair(61, 10),
- Pair(101, 12),
- Pair(131, 15),
- Pair(181, 18),
- Pair(251, 15),
- Pair(301, 12),
- Pair(360, 12)
- )
-
- override fun get(sourceColor: Cam): Double {
- return getHueRotation(sourceColor.hue, hueToRotations)
- }
-}
-
-internal class HueVibrantTertiary() : Hue {
- val hueToRotations =
- listOf(
- Pair(0, 35),
- Pair(41, 30),
- Pair(61, 20),
- Pair(101, 25),
- Pair(131, 30),
- Pair(181, 35),
- Pair(251, 30),
- Pair(301, 25),
- Pair(360, 25)
- )
-
- override fun get(sourceColor: Cam): Double {
- return getHueRotation(sourceColor.hue, hueToRotations)
- }
-}
-
-internal class HueExpressiveSecondary() : Hue {
- val hueToRotations =
- listOf(
- Pair(0, 45),
- Pair(21, 95),
- Pair(51, 45),
- Pair(121, 20),
- Pair(151, 45),
- Pair(191, 90),
- Pair(271, 45),
- Pair(321, 45),
- Pair(360, 45)
- )
-
- override fun get(sourceColor: Cam): Double {
- return getHueRotation(sourceColor.hue, hueToRotations)
- }
-}
-
-internal class HueExpressiveTertiary() : Hue {
- val hueToRotations =
- listOf(
- Pair(0, 120),
- Pair(21, 120),
- Pair(51, 20),
- Pair(121, 45),
- Pair(151, 20),
- Pair(191, 15),
- Pair(271, 20),
- Pair(321, 120),
- Pair(360, 120)
- )
-
- override fun get(sourceColor: Cam): Double {
- return getHueRotation(sourceColor.hue, hueToRotations)
- }
-}
-
-internal interface Chroma {
- fun get(sourceColor: Cam): Double
-
- companion object {
- val MAX_VALUE = 120.0
- val MIN_VALUE = 0.0
- }
-}
-
-internal class ChromaMaxOut : Chroma {
- override fun get(sourceColor: Cam): Double {
- // Intentionally high. Gamut mapping from impossible HCT to sRGB will ensure that
- // the maximum chroma is reached, even if lower than this constant.
- return Chroma.MAX_VALUE + 10.0
- }
-}
-
-internal class ChromaMultiple(val multiple: Double) : Chroma {
- override fun get(sourceColor: Cam): Double {
- return sourceColor.chroma * multiple
- }
-}
-
-internal class ChromaAdd(val amount: Double) : Chroma {
- override fun get(sourceColor: Cam): Double {
- return sourceColor.chroma + amount
- }
-}
-
-internal class ChromaBound(
- val baseChroma: Chroma,
- val minVal: Double,
- val maxVal: Double,
-) : Chroma {
- override fun get(sourceColor: Cam): Double {
- val result = baseChroma.get(sourceColor)
- return min(max(result, minVal), maxVal)
- }
-}
-
-internal class ChromaConstant(val chroma: Double) : Chroma {
- override fun get(sourceColor: Cam): Double {
- return chroma
- }
-}
-
-internal class ChromaSource : Chroma {
- override fun get(sourceColor: Cam): Double {
- return sourceColor.chroma.toDouble()
- }
-}
-
-internal class TonalSpec(val hue: Hue = HueSource(), val chroma: Chroma) {
- fun shades(sourceColor: Cam): List<Int> {
- val hue = hue.get(sourceColor)
- val chroma = chroma.get(sourceColor)
- return Shades.of(hue.toFloat(), chroma.toFloat()).toList()
- }
-
- fun getAtTone(sourceColor: Cam, tone: Float): Int {
- val hue = hue.get(sourceColor)
- val chroma = chroma.get(sourceColor)
- return ColorUtils.CAMToColor(hue.toFloat(), chroma.toFloat(), (1000f - tone) / 10f)
- }
-}
-
-internal class CoreSpec(
- val a1: TonalSpec,
- val a2: TonalSpec,
- val a3: TonalSpec,
- val n1: TonalSpec,
- val n2: TonalSpec
-)
-
-enum class Style(internal val coreSpec: CoreSpec) {
- SPRITZ(
- CoreSpec(
- a1 = TonalSpec(HueSource(), ChromaConstant(12.0)),
- a2 = TonalSpec(HueSource(), ChromaConstant(8.0)),
- a3 = TonalSpec(HueSource(), ChromaConstant(16.0)),
- n1 = TonalSpec(HueSource(), ChromaConstant(2.0)),
- n2 = TonalSpec(HueSource(), ChromaConstant(2.0))
- )
- ),
- TONAL_SPOT(
- CoreSpec(
- a1 = TonalSpec(HueSource(), ChromaConstant(36.0)),
- a2 = TonalSpec(HueSource(), ChromaConstant(16.0)),
- a3 = TonalSpec(HueAdd(60.0), ChromaConstant(24.0)),
- n1 = TonalSpec(HueSource(), ChromaConstant(6.0)),
- n2 = TonalSpec(HueSource(), ChromaConstant(8.0))
- )
- ),
- VIBRANT(
- CoreSpec(
- a1 = TonalSpec(HueSource(), ChromaMaxOut()),
- a2 = TonalSpec(HueVibrantSecondary(), ChromaConstant(24.0)),
- a3 = TonalSpec(HueVibrantTertiary(), ChromaConstant(32.0)),
- n1 = TonalSpec(HueSource(), ChromaConstant(10.0)),
- n2 = TonalSpec(HueSource(), ChromaConstant(12.0))
- )
- ),
- EXPRESSIVE(
- CoreSpec(
- a1 = TonalSpec(HueAdd(240.0), ChromaConstant(40.0)),
- a2 = TonalSpec(HueExpressiveSecondary(), ChromaConstant(24.0)),
- a3 = TonalSpec(HueExpressiveTertiary(), ChromaConstant(32.0)),
- n1 = TonalSpec(HueAdd(15.0), ChromaConstant(8.0)),
- n2 = TonalSpec(HueAdd(15.0), ChromaConstant(12.0))
- )
- ),
- RAINBOW(
- CoreSpec(
- a1 = TonalSpec(HueSource(), ChromaConstant(48.0)),
- a2 = TonalSpec(HueSource(), ChromaConstant(16.0)),
- a3 = TonalSpec(HueAdd(60.0), ChromaConstant(24.0)),
- n1 = TonalSpec(HueSource(), ChromaConstant(0.0)),
- n2 = TonalSpec(HueSource(), ChromaConstant(0.0))
- )
- ),
- FRUIT_SALAD(
- CoreSpec(
- a1 = TonalSpec(HueSubtract(50.0), ChromaConstant(48.0)),
- a2 = TonalSpec(HueSubtract(50.0), ChromaConstant(36.0)),
- a3 = TonalSpec(HueSource(), ChromaConstant(36.0)),
- n1 = TonalSpec(HueSource(), ChromaConstant(10.0)),
- n2 = TonalSpec(HueSource(), ChromaConstant(16.0))
- )
- ),
- CONTENT(
- CoreSpec(
- a1 = TonalSpec(HueSource(), ChromaSource()),
- a2 = TonalSpec(HueSource(), ChromaMultiple(0.33)),
- a3 = TonalSpec(HueSource(), ChromaMultiple(0.66)),
- n1 = TonalSpec(HueSource(), ChromaMultiple(0.0833)),
- n2 = TonalSpec(HueSource(), ChromaMultiple(0.1666))
- )
- ),
- MONOCHROMATIC(
- CoreSpec(
- a1 = TonalSpec(HueSource(), ChromaConstant(.0)),
- a2 = TonalSpec(HueSource(), ChromaConstant(.0)),
- a3 = TonalSpec(HueSource(), ChromaConstant(.0)),
- n1 = TonalSpec(HueSource(), ChromaConstant(.0)),
- n2 = TonalSpec(HueSource(), ChromaConstant(.0))
- )
- ),
- CLOCK(
- CoreSpec(
- a1 = TonalSpec(HueSource(), ChromaBound(ChromaSource(), 20.0, Chroma.MAX_VALUE)),
- a2 = TonalSpec(HueAdd(10.0), ChromaBound(ChromaMultiple(0.85), 17.0, 40.0)),
- a3 = TonalSpec(HueAdd(20.0), ChromaBound(ChromaAdd(20.0), 50.0, Chroma.MAX_VALUE)),
-
- // Not Used
- n1 = TonalSpec(HueSource(), ChromaConstant(0.0)),
- n2 = TonalSpec(HueSource(), ChromaConstant(0.0))
- )
- ),
- CLOCK_VIBRANT(
- CoreSpec(
- a1 = TonalSpec(HueSource(), ChromaBound(ChromaSource(), 70.0, Chroma.MAX_VALUE)),
- a2 = TonalSpec(HueAdd(20.0), ChromaBound(ChromaSource(), 70.0, Chroma.MAX_VALUE)),
- a3 = TonalSpec(HueAdd(60.0), ChromaBound(ChromaSource(), 70.0, Chroma.MAX_VALUE)),
-
- // Not Used
- n1 = TonalSpec(HueSource(), ChromaConstant(0.0)),
- n2 = TonalSpec(HueSource(), ChromaConstant(0.0))
- )
- )
+enum class Style{
+ SPRITZ,
+ TONAL_SPOT,
+ VIBRANT,
+ EXPRESSIVE,
+ RAINBOW,
+ FRUIT_SALAD,
+ CONTENT,
+ MONOCHROMATIC,
+ CLOCK,
+ CLOCK_VIBRANT
}
class TonalPalette
internal constructor(
- private val spec: TonalSpec,
- seedColor: Int,
+ private val materialTonalPalette: com.google.ux.material.libmonet.palettes.TonalPalette
) {
- val seedCam: Cam = Cam.fromInt(seedColor)
- val allShades: List<Int> = spec.shades(seedCam)
- val allShadesMapped: Map<Int, Int> = SHADE_KEYS.zip(allShades).toMap()
- val baseColor: Int
+ @Deprecated("Do not use. For color system only")
+ val allShades: List<Int>
+ val allShadesMapped: Map<Int, Int>
- init {
- val h = spec.hue.get(seedCam).toFloat()
- val c = spec.chroma.get(seedCam).toFloat()
- baseColor = ColorUtils.CAMToColor(h, c, CamUtils.lstarFromInt(seedColor))
+ init{
+ allShades = SHADE_KEYS.map {key -> getAtTone(key.toFloat()) }
+ allShadesMapped = SHADE_KEYS.zip(allShades).toMap()
}
// Dynamically computed tones across the full range from 0 to 1000
- fun getAtTone(tone: Float) = spec.getAtTone(seedCam, tone)
+ fun getAtTone(shade: Float): Int = materialTonalPalette.tone(((1000.0f - shade) / 10f).toInt())
// Predefined & precomputed tones
- val s10: Int
+ val s0: Int
get() = this.allShades[0]
- val s50: Int
+ val s10: Int
get() = this.allShades[1]
- val s100: Int
+ val s50: Int
get() = this.allShades[2]
- val s200: Int
+ val s100: Int
get() = this.allShades[3]
- val s300: Int
+ val s200: Int
get() = this.allShades[4]
- val s400: Int
+ val s300: Int
get() = this.allShades[5]
- val s500: Int
+ val s400: Int
get() = this.allShades[6]
- val s600: Int
+ val s500: Int
get() = this.allShades[7]
- val s700: Int
+ val s600: Int
get() = this.allShades[8]
- val s800: Int
+ val s700: Int
get() = this.allShades[9]
- val s900: Int
+ val s800: Int
get() = this.allShades[10]
- val s1000: Int
+ val s900: Int
get() = this.allShades[11]
+ val s1000: Int
+ get() = this.allShades[12]
companion object {
- val SHADE_KEYS = listOf(10, 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000)
+ val SHADE_KEYS = listOf(0, 10, 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000)
}
}
@@ -381,9 +105,20 @@
"instead")
class ColorScheme(
@ColorInt val seed: Int,
- val darkTheme: Boolean,
- val style: Style = Style.TONAL_SPOT
+ val isDark: Boolean,
+ val style: Style,
+ val contrastLevel: Double
) {
+ var materialScheme: DynamicScheme
+
+ private val proposedSeedHct: Hct = Hct.fromInt(seed)
+ private val seedHct: Hct = Hct.fromInt(if (seed == Color.TRANSPARENT) {
+ GOOGLE_BLUE
+ } else if (style != Style.CONTENT && proposedSeedHct.chroma < 5) {
+ GOOGLE_BLUE
+ } else {
+ seed
+ })
val accent1: TonalPalette
val accent2: TonalPalette
@@ -395,62 +130,49 @@
@JvmOverloads
constructor(
+ @ColorInt seed: Int,
+ darkTheme: Boolean,
+ style: Style
+ ) : this(seed, darkTheme, style, 0.5)
+
+ @JvmOverloads
+ constructor(
wallpaperColors: WallpaperColors,
darkTheme: Boolean,
style: Style = Style.TONAL_SPOT
) : this(getSeedColor(wallpaperColors, style != Style.CONTENT), darkTheme, style)
- val allHues: List<TonalPalette>
- get() {
- return listOf(accent1, accent2, accent3, neutral1, neutral2)
- }
-
- val allAccentColors: List<Int>
- get() {
- val allColors = mutableListOf<Int>()
- allColors.addAll(accent1.allShades)
- allColors.addAll(accent2.allShades)
- allColors.addAll(accent3.allShades)
- return allColors
- }
-
- val allNeutralColors: List<Int>
- get() {
- val allColors = mutableListOf<Int>()
- allColors.addAll(neutral1.allShades)
- allColors.addAll(neutral2.allShades)
- return allColors
- }
-
val backgroundColor
- get() = ColorUtils.setAlphaComponent(if (darkTheme) neutral1.s700 else neutral1.s10, 0xFF)
+ get() = ColorUtils.setAlphaComponent(if (isDark) neutral1.s700 else neutral1.s10, 0xFF)
val accentColor
- get() = ColorUtils.setAlphaComponent(if (darkTheme) accent1.s100 else accent1.s500, 0xFF)
+ get() = ColorUtils.setAlphaComponent(if (isDark) accent1.s100 else accent1.s500, 0xFF)
init {
- val proposedSeedCam = Cam.fromInt(seed)
- val seedArgb =
- if (seed == Color.TRANSPARENT) {
- GOOGLE_BLUE
- } else if (style != Style.CONTENT && proposedSeedCam.chroma < 5) {
- GOOGLE_BLUE
- } else {
- seed
- }
+ materialScheme = when (style) {
+ Style.SPRITZ -> SchemeNeutral(seedHct, isDark, contrastLevel)
+ Style.TONAL_SPOT -> SchemeTonalSpot(seedHct, isDark, contrastLevel)
+ Style.VIBRANT -> SchemeVibrant(seedHct, isDark, contrastLevel)
+ Style.EXPRESSIVE -> SchemeExpressive(seedHct, isDark, contrastLevel)
+ Style.RAINBOW -> SchemeRainbow(seedHct, isDark, contrastLevel)
+ Style.FRUIT_SALAD -> SchemeFruitSalad(seedHct, isDark, contrastLevel)
+ Style.CONTENT -> SchemeContent(seedHct, isDark, contrastLevel)
+ Style.MONOCHROMATIC -> SchemeMonochrome(seedHct, isDark, contrastLevel)
- accent1 = TonalPalette(style.coreSpec.a1, seedArgb)
- accent2 = TonalPalette(style.coreSpec.a2, seedArgb)
- accent3 = TonalPalette(style.coreSpec.a3, seedArgb)
- neutral1 = TonalPalette(style.coreSpec.n1, seedArgb)
- neutral2 = TonalPalette(style.coreSpec.n2, seedArgb)
+ // SystemUI Schemes
+ Style.CLOCK -> SchemeClock(seedHct, isDark, contrastLevel)
+ Style.CLOCK_VIBRANT -> SchemeClockVibrant(seedHct, isDark, contrastLevel)
+ }
+
+ accent1 = TonalPalette(materialScheme.primaryPalette)
+ accent2 = TonalPalette(materialScheme.secondaryPalette)
+ accent3 = TonalPalette(materialScheme.tertiaryPalette)
+ neutral1 = TonalPalette(materialScheme.neutralPalette)
+ neutral2 = TonalPalette(materialScheme.neutralVariantPalette)
}
- val shadeCount
- get() = this.accent1.allShades.size
-
val seedTone: Float
- get() = 1000f - CamUtils.lstarFromInt(seed) * 10f
+ get() = 1000f - proposedSeedHct.tone.toFloat() * 10f
override fun toString(): String {
return "ColorScheme {\n" +
@@ -507,7 +229,7 @@
if (!filter) {
true
} else {
- Cam.fromInt(it).chroma >= MIN_CHROMA
+ Hct.fromInt(it).chroma >= MIN_CHROMA
}
}
.toList()
@@ -519,15 +241,15 @@
val intToProportion =
wallpaperColors.allColors.mapValues { it.value.toDouble() / totalPopulation }
- val intToCam = wallpaperColors.allColors.mapValues { Cam.fromInt(it.key) }
+ val intToHct = wallpaperColors.allColors.mapValues { Hct.fromInt(it.key) }
// Get an array with 360 slots. A slot contains the percentage of colors with that hue.
- val hueProportions = huePopulations(intToCam, intToProportion, filter)
+ val hueProportions = huePopulations(intToHct, intToProportion, filter)
// 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()
+ val hct = intToHct[it.key]!!
+ val hue = hct.hue.roundToInt()
var proportion = 0.0
for (i in hue - 15..hue + 15) {
proportion += hueProportions[wrapDegrees(i)]
@@ -537,18 +259,18 @@
// 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 =
- if (!filter) intToCam
+ val filteredIntToHct =
+ if (!filter) intToHct
else
- (intToCam.filter {
- val cam = it.value
+ (intToHct.filter {
+ val hct = it.value
val proportion = intToHueProportion[it.key]!!
- cam.chroma >= MIN_CHROMA &&
+ hct.chroma >= MIN_CHROMA &&
(totalPopulationMeaningless || proportion > 0.01)
})
// Sort the colors by score, from high to low.
val intToScoreIntermediate =
- filteredIntToCam.mapValues { score(it.value, intToHueProportion[it.key]!!) }
+ filteredIntToHct.mapValues { score(it.value, intToHueProportion[it.key]!!) }
val intToScore = intToScoreIntermediate.entries.toMutableList()
intToScore.sortByDescending { it.value }
@@ -564,8 +286,8 @@
val int = entry.key
val existingSeedNearby =
seeds.find {
- val hueA = intToCam[int]!!.hue
- val hueB = intToCam[it]!!.hue
+ val hueA = intToHct[int]!!.hue
+ val hueB = intToHct[it]!!.hue
hueDiff(hueA, hueB) < i
} != null
if (existingSeedNearby) {
@@ -600,30 +322,16 @@
}
}
- public fun wrapDegreesDouble(degrees: Double): Double {
- return when {
- degrees < 0 -> {
- (degrees % 360) + 360
- }
- degrees >= 360 -> {
- degrees % 360
- }
- else -> {
- degrees
- }
- }
- }
-
- private fun hueDiff(a: Float, b: Float): Float {
+ private fun hueDiff(a: Double, b: Double): Double {
return 180f - ((a - b).absoluteValue - 180f).absoluteValue
}
private fun stringForColor(color: Int): String {
val width = 4
- val hct = Cam.fromInt(color)
+ val hct = Hct.fromInt(color)
val h = "H${hct.hue.roundToInt().toString().padEnd(width)}"
val c = "C${hct.chroma.roundToInt().toString().padEnd(width)}"
- val t = "T${CamUtils.lstarFromInt(color).roundToInt().toString().padEnd(width)}"
+ val t = "T${hct.tone.roundToInt().toString().padEnd(width)}"
val hex = Integer.toHexString(color and 0xffffff).padStart(6, '0').uppercase()
return "$h$c$t = #$hex"
}
@@ -633,16 +341,16 @@
colors.map { stringForColor(it) }.joinToString(separator = "\n") { it }
}
- private fun score(cam: Cam, proportion: Double): Double {
+ private fun score(hct: Hct, 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)
+ if (hct.chroma < ACCENT1_CHROMA) 0.1 * (hct.chroma - ACCENT1_CHROMA)
+ else 0.3 * (hct.chroma - ACCENT1_CHROMA)
return chromaScore + proportionScore
}
private fun huePopulations(
- camByColor: Map<Int, Cam>,
+ hctByColor: Map<Int, Hct>,
populationByColor: Map<Int, Double>,
filter: Boolean = true
): List<Double> {
@@ -650,9 +358,9 @@
for (entry in populationByColor.entries) {
val population = populationByColor[entry.key]!!
- val cam = camByColor[entry.key]!!
- val hue = cam.hue.roundToInt() % 360
- if (filter && cam.chroma <= MIN_CHROMA) {
+ val hct = hctByColor[entry.key]!!
+ val hue = hct.hue.roundToInt() % 360
+ if (filter && hct.chroma <= MIN_CHROMA) {
continue
}
huePopulation[hue] = huePopulation[hue] + population
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/SchemeClock.java b/packages/SystemUI/monet/src/com/android/systemui/monet/SchemeClock.java
new file mode 100644
index 0000000..4747cc5
--- /dev/null
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/SchemeClock.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2024 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 static com.google.ux.material.libmonet.utils.MathUtils.clampDouble;
+
+import static java.lang.Double.max;
+
+import com.google.ux.material.libmonet.hct.Hct;
+import com.google.ux.material.libmonet.palettes.TonalPalette;
+import com.google.ux.material.libmonet.scheme.DynamicScheme;
+import com.google.ux.material.libmonet.scheme.Variant;
+
+public class SchemeClock extends DynamicScheme {
+ public SchemeClock(Hct sourceColorHct, boolean isDark, double contrastLevel) {
+ super(
+ sourceColorHct,
+ Variant.MONOCHROME,
+ isDark,
+ contrastLevel,
+ /*primary*/
+ TonalPalette.fromHueAndChroma(
+ /*hue*/ sourceColorHct.getHue(),
+ /*chroma*/ max(sourceColorHct.getChroma(), 20)
+ ),
+ /*secondary*/
+ TonalPalette.fromHueAndChroma(
+ /*hue*/ sourceColorHct.getHue() + 10.0,
+ /*chroma*/ clampDouble(17, 40, sourceColorHct.getChroma() * 0.85)
+ ),
+ /*tertiary*/
+ TonalPalette.fromHueAndChroma(
+ /*hue*/ sourceColorHct.getHue() + 20.0,
+ /*chroma*/ max(sourceColorHct.getChroma() + 20, 50)
+ ),
+
+ //not used
+ TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 0.0),
+ TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 0.0));
+ }
+}
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/SchemeClockVibrant.java b/packages/SystemUI/monet/src/com/android/systemui/monet/SchemeClockVibrant.java
new file mode 100644
index 0000000..fb5e972
--- /dev/null
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/SchemeClockVibrant.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2024 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 static java.lang.Double.max;
+
+import com.google.ux.material.libmonet.hct.Hct;
+import com.google.ux.material.libmonet.palettes.TonalPalette;
+import com.google.ux.material.libmonet.scheme.DynamicScheme;
+import com.google.ux.material.libmonet.scheme.Variant;
+
+public class SchemeClockVibrant extends DynamicScheme {
+ public SchemeClockVibrant(Hct sourceColorHct, boolean isDark, double contrastLevel) {
+ super(
+ sourceColorHct,
+ Variant.MONOCHROME,
+ isDark,
+ contrastLevel,
+ /*primary*/
+ TonalPalette.fromHueAndChroma(
+ /*hue*/ sourceColorHct.getHue(),
+ /*chroma*/ max(sourceColorHct.getChroma(), 70)
+ ),
+ /*secondary*/
+ TonalPalette.fromHueAndChroma(
+ /*hue*/ sourceColorHct.getHue() + 20.0,
+ /*chroma*/ max(sourceColorHct.getChroma(), 70)
+ ),
+ /*tertiary*/
+ TonalPalette.fromHueAndChroma(
+ /*hue*/ sourceColorHct.getHue() + 60.0,
+ /*chroma*/ max(sourceColorHct.getChroma(), 70)
+ ),
+
+ //not used
+ TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 0.0),
+ TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 0.0));
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
index b5efc44..aac18e3 100644
--- a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
+++ b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
@@ -86,15 +86,6 @@
import com.android.systemui.util.settings.SecureSettings;
import com.google.ux.material.libmonet.dynamiccolor.MaterialDynamicColors;
-import com.google.ux.material.libmonet.hct.Hct;
-import com.google.ux.material.libmonet.scheme.DynamicScheme;
-import com.google.ux.material.libmonet.scheme.SchemeExpressive;
-import com.google.ux.material.libmonet.scheme.SchemeFruitSalad;
-import com.google.ux.material.libmonet.scheme.SchemeMonochrome;
-import com.google.ux.material.libmonet.scheme.SchemeNeutral;
-import com.google.ux.material.libmonet.scheme.SchemeRainbow;
-import com.google.ux.material.libmonet.scheme.SchemeTonalSpot;
-import com.google.ux.material.libmonet.scheme.SchemeVibrant;
import org.json.JSONException;
import org.json.JSONObject;
@@ -150,7 +141,7 @@
// Dominant color extracted from wallpaper, NOT the color used on the overlay
protected int mMainWallpaperColor = Color.TRANSPARENT;
// UI contrast as reported by UiModeManager
- private float mContrast = 0;
+ private double mContrast = 0.0;
// Theme variant: Vibrant, Tonal, Expressive, etc
@VisibleForTesting
protected Style mThemeStyle = Style.TONAL_SPOT;
@@ -170,8 +161,8 @@
private final JavaAdapter mJavaAdapter;
private final KeyguardTransitionInteractor mKeyguardTransitionInteractor;
private final UiModeManager mUiModeManager;
- private DynamicScheme mDynamicSchemeDark;
- private DynamicScheme mDynamicSchemeLight;
+ private ColorScheme mDarkColorScheme;
+ private ColorScheme mLightColorScheme;
// Defers changing themes until Setup Wizard is done.
private boolean mDeferredThemeEvaluation;
@@ -210,7 +201,7 @@
boolean currentUser = userId == mUserTracker.getUserId();
boolean isAsleep = themeOverlayControllerWakefulnessDeprecation()
? mKeyguardTransitionInteractor.isFinishedInStateWhereValue(
- state -> KeyguardState.Companion.deviceIsAsleepInState(state))
+ KeyguardState.Companion::deviceIsAsleepInState)
: mWakefulnessLifecycle.getWakefulness() != WAKEFULNESS_ASLEEP;
if (currentUser && !mAcceptColorEvents && isAsleep) {
@@ -483,19 +474,14 @@
reevaluateSystemTheme(true /* forceReload */);
});
+ // All wallpaper color and keyguard logic only applies when Monet is enabled.
if (!mIsMonetEnabled) {
return;
}
mUserTracker.addCallback(mUserTrackerCallback, mMainExecutor);
-
mDeviceProvisionedController.addCallback(mDeviceProvisionedListener);
- // All wallpaper color and keyguard logic only applies when Monet is enabled.
- if (!mIsMonetEnabled) {
- return;
- }
-
// Upon boot, make sure we have the most up to date colors
Runnable updateColors = () -> {
WallpaperColors systemColor = mWallpaperManager.getWallpaperColors(
@@ -589,29 +575,6 @@
return ColorScheme.getSeedColor(wallpaperColors);
}
- private static DynamicScheme dynamicSchemeFromStyle(Style style, int color,
- boolean isDark, double contrastLevel) {
- Hct sourceColorHct = Hct.fromInt(color);
- switch (style) {
- case EXPRESSIVE:
- return new SchemeExpressive(sourceColorHct, isDark, contrastLevel);
- case SPRITZ:
- return new SchemeNeutral(sourceColorHct, isDark, contrastLevel);
- case TONAL_SPOT:
- return new SchemeTonalSpot(sourceColorHct, isDark, contrastLevel);
- case FRUIT_SALAD:
- return new SchemeFruitSalad(sourceColorHct, isDark, contrastLevel);
- case RAINBOW:
- return new SchemeRainbow(sourceColorHct, isDark, contrastLevel);
- case VIBRANT:
- return new SchemeVibrant(sourceColorHct, isDark, contrastLevel);
- case MONOCHROMATIC:
- return new SchemeMonochrome(sourceColorHct, isDark, contrastLevel);
- default:
- return null;
- }
- }
-
@VisibleForTesting
protected boolean isNightMode() {
return (mResources.getConfiguration().uiMode
@@ -626,22 +589,17 @@
@VisibleForTesting
protected boolean isPrivateProfile(UserHandle userHandle) {
Context usercontext = mContext.createContextAsUser(userHandle,0);
- if (usercontext.getSystemService(UserManager.class).isPrivateProfile()) {
- return true;
- }
- return false;
+ return usercontext.getSystemService(UserManager.class).isPrivateProfile();
}
private void createOverlays(int color) {
- boolean nightMode = isNightMode();
- mColorScheme = new ColorScheme(color, nightMode, mThemeStyle);
+ mDarkColorScheme = new ColorScheme(color, true /* isDark */, mThemeStyle, mContrast);
+ mLightColorScheme = new ColorScheme(color, false /* isDark */, mThemeStyle, mContrast);
+ mColorScheme = isNightMode() ? mDarkColorScheme : mLightColorScheme;
+
mNeutralOverlay = createNeutralOverlay();
mSecondaryOverlay = createAccentOverlay();
- mDynamicSchemeDark = dynamicSchemeFromStyle(
- mThemeStyle, color, true /* isDark */, mContrast);
- mDynamicSchemeLight = dynamicSchemeFromStyle(
- mThemeStyle, color, false /* isDark */, mContrast);
mDynamicOverlay = createDynamicOverlay();
}
@@ -682,10 +640,10 @@
private void assignDynamicPaletteToOverlay(FabricatedOverlay overlay, boolean isDark) {
String suffix = isDark ? "dark" : "light";
- DynamicScheme scheme = isDark ? mDynamicSchemeDark : mDynamicSchemeLight;
+ ColorScheme scheme = isDark ? mDarkColorScheme : mLightColorScheme;
DynamicColors.allDynamicColorsMapped(mIsFidelityEnabled).forEach(p -> {
String resourceName = "android:color/system_" + p.first + "_" + suffix;
- int colorValue = p.second.getArgb(scheme);
+ int colorValue = p.second.getArgb(scheme.getMaterialScheme());
overlay.setResourceValue(resourceName, TYPE_INT_COLOR_ARGB8, colorValue,
null /* configuration */);
});
@@ -694,7 +652,7 @@
private void assignFixedColorsToOverlay(FabricatedOverlay overlay) {
DynamicColors.getFixedColorsMapped(mIsFidelityEnabled).forEach(p -> {
String resourceName = "android:color/system_" + p.first;
- int colorValue = p.second.getArgb(mDynamicSchemeLight);
+ int colorValue = p.second.getArgb(mLightColorScheme.getMaterialScheme());
overlay.setResourceValue(resourceName, TYPE_INT_COLOR_ARGB8, colorValue,
null /* configuration */);
});
@@ -702,6 +660,7 @@
/**
* Checks if the color scheme in mColorScheme matches the current system palettes.
+ *
* @param managedProfiles List of managed profiles for this user.
*/
private boolean colorSchemeIsApplied(Set<UserHandle> managedProfiles) {
@@ -723,15 +682,18 @@
&& res.getColor(android.R.color.system_neutral2_500, theme)
== mColorScheme.getNeutral2().getS500()
&& res.getColor(android.R.color.system_outline_variant_dark, theme)
- == dynamicColors.outlineVariant().getArgb(mDynamicSchemeDark)
+ == dynamicColors.outlineVariant().getArgb(mDarkColorScheme.getMaterialScheme())
&& res.getColor(android.R.color.system_outline_variant_light, theme)
- == dynamicColors.outlineVariant().getArgb(mDynamicSchemeLight)
+ == dynamicColors.outlineVariant().getArgb(mLightColorScheme.getMaterialScheme())
&& res.getColor(android.R.color.system_primary_container_dark, theme)
- == dynamicColors.primaryContainer().getArgb(mDynamicSchemeDark)
+ == dynamicColors.primaryContainer().getArgb(
+ mDarkColorScheme.getMaterialScheme())
&& res.getColor(android.R.color.system_primary_container_light, theme)
- == dynamicColors.primaryContainer().getArgb(mDynamicSchemeLight)
+ == dynamicColors.primaryContainer().getArgb(
+ mLightColorScheme.getMaterialScheme())
&& res.getColor(android.R.color.system_primary_fixed, theme)
- == dynamicColors.primaryFixed().getArgb(mDynamicSchemeLight))) {
+ == dynamicColors.primaryFixed().getArgb(
+ mLightColorScheme.getMaterialScheme()))) {
return false;
}
}
diff --git a/packages/SystemUI/tests/AndroidTest.xml b/packages/SystemUI/tests/AndroidTest.xml
index 31e7bd2..cd2a62d 100644
--- a/packages/SystemUI/tests/AndroidTest.xml
+++ b/packages/SystemUI/tests/AndroidTest.xml
@@ -32,4 +32,11 @@
<option name="test-filter-dir" value="/data/data/com.android.systemui.tests" />
<option name="hidden-api-checks" value="false"/>
</test>
+
+ <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
+ <option name="directory-keys"
+ value="/data/user/0/com.android.systemui.tests/files"/>
+ <option name="collect-on-run-ended-only" value="true"/>
+ <option name="clean-up" value="true"/>
+ </metrics_collector>
</configuration>
diff --git a/packages/SystemUI/tests/src/com/android/systemui/monet/ColorSchemeTest.kt b/packages/SystemUI/tests/src/com/android/systemui/monet/ColorSchemeTest.kt
new file mode 100644
index 0000000..261e8c0
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/monet/ColorSchemeTest.kt
@@ -0,0 +1,249 @@
+/*
+ * 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.test.suitebuilder.annotation.SmallTest
+import android.testing.AndroidTestingRunner
+import android.util.Log
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.theme.DynamicColors
+import com.google.ux.material.libmonet.hct.Hct
+import com.google.ux.material.libmonet.scheme.SchemeTonalSpot
+import java.io.File
+import java.io.FileWriter
+import java.io.StringWriter
+import javax.xml.parsers.DocumentBuilderFactory
+import javax.xml.transform.OutputKeys
+import javax.xml.transform.TransformerException
+import javax.xml.transform.TransformerFactory
+import javax.xml.transform.dom.DOMSource
+import javax.xml.transform.stream.StreamResult
+import kotlin.math.abs
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.w3c.dom.Document
+import org.w3c.dom.Element
+import org.w3c.dom.Node
+
+private const val fileHeader =
+ """
+ ~ Copyright (C) 2022 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.
+"""
+
+private fun testName(name: String): String {
+ return "Auto generated by: atest ColorSchemeTest#$name"
+}
+
+private const val commentRoles =
+ "Colors used in Android system, from design system. These " +
+ "values can be overlaid at runtime by OverlayManager RROs."
+
+private const val commentOverlay = "This value can be overlaid at runtime by OverlayManager RROs."
+
+private fun commentWhite(paletteName: String): String {
+ return "Lightest shade of the $paletteName color used by the system. White. $commentOverlay"
+}
+
+private fun commentBlack(paletteName: String): String {
+ return "Darkest shade of the $paletteName color used by the system. Black. $commentOverlay"
+}
+
+private fun commentShade(paletteName: String, tone: Int): String {
+ return "Shade of the $paletteName system color at $tone% perceptual luminance (L* in L*a*b* " +
+ "color space). $commentOverlay"
+}
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class ColorSchemeTest : SysuiTestCase() {
+ @Test
+ fun generateThemeStyles() {
+ val document = buildDoc<Any>()
+
+ val themes = document.createElement("themes")
+ document.appendWithBreak(themes)
+
+ var hue = 0.0
+ while (hue < 360) {
+ val sourceColor = Hct.from(hue, 50.0, 50.0)
+ val sourceColorHex = sourceColor.toInt().toRGBHex()
+
+ val theme = document.createElement("theme")
+ theme.setAttribute("color", sourceColorHex)
+ themes.appendChild(theme)
+
+ for (styleValue in Style.entries) {
+ if (
+ styleValue == Style.CLOCK ||
+ styleValue == Style.CLOCK_VIBRANT ||
+ styleValue == Style.CONTENT
+ ) {
+ continue
+ }
+
+ val style = document.createElement(styleValue.name.lowercase())
+ val colorScheme = ColorScheme(sourceColor.toInt(), false, styleValue)
+
+ style.appendChild(
+ document.createTextNode(
+ listOf(
+ colorScheme.accent1,
+ colorScheme.accent2,
+ colorScheme.accent3,
+ colorScheme.neutral1,
+ colorScheme.neutral2
+ )
+ .flatMap { a -> listOf(*a.allShades.toTypedArray()) }
+ .joinToString(",", transform = Int::toRGBHex)
+ )
+ )
+ theme.appendChild(style)
+ }
+
+ hue += 60
+ }
+
+ saveFile(document, "current_themes.xml")
+ }
+
+ @Test
+ fun generateDefaultValues() {
+ val document = buildDoc<Any>()
+
+ val resources = document.createElement("resources")
+ document.appendWithBreak(resources)
+
+ // shade colors
+ val colorScheme = ColorScheme(GOOGLE_BLUE, false)
+ arrayOf(
+ Triple("accent1", "Primary", colorScheme.accent1),
+ Triple("accent2", "Secondary", colorScheme.accent2),
+ Triple("accent3", "Tertiary", colorScheme.accent3),
+ Triple("neutral1", "Neutral", colorScheme.neutral1),
+ Triple("neutral2", "Secondary Neutral", colorScheme.neutral2)
+ )
+ .forEach {
+ val (paletteName, readable, palette) = it
+ palette.allShadesMapped.entries.forEachIndexed { index, (shade, colorValue) ->
+ val comment =
+ when (index) {
+ 0 -> commentWhite(readable)
+ palette.allShadesMapped.entries.size - 1 -> commentBlack(readable)
+ else -> commentShade(readable, abs(shade / 10 - 100))
+ }
+ resources.createColorEntry("system_${paletteName}_$shade", colorValue, comment)
+ }
+ }
+
+ resources.appendWithBreak(document.createComment(commentRoles), 2)
+
+ // dynamic colors
+ arrayOf(false, true).forEach { isDark ->
+ val suffix = if (isDark) "_dark" else "_light"
+ val dynamicScheme = SchemeTonalSpot(Hct.fromInt(GOOGLE_BLUE), isDark, 0.5)
+ DynamicColors.allDynamicColorsMapped(false).forEach {
+ resources.createColorEntry(
+ "system_${it.first}$suffix",
+ it.second.getArgb(dynamicScheme)
+ )
+ }
+ }
+
+ // fixed colors
+ val dynamicScheme = SchemeTonalSpot(Hct.fromInt(GOOGLE_BLUE), false, 0.5)
+ DynamicColors.getFixedColorsMapped(false).forEach {
+ resources.createColorEntry("system_${it.first}", it.second.getArgb(dynamicScheme))
+ }
+
+ saveFile(document, "role_values.xml")
+ }
+
+ // Helper Functions
+
+ private inline fun <reified T> buildDoc(): Document {
+ val functionName = T::class.simpleName + ""
+ val factory = DocumentBuilderFactory.newInstance()
+ val builder = factory.newDocumentBuilder()
+ val document = builder.newDocument()
+
+ document.appendWithBreak(document.createComment(fileHeader))
+ document.appendWithBreak(document.createComment(testName(functionName)))
+
+ return document
+ }
+
+ private fun documentToString(document: Document): String {
+ try {
+ val transformerFactory = TransformerFactory.newInstance()
+ val transformer = transformerFactory.newTransformer()
+ transformer.setOutputProperty(OutputKeys.MEDIA_TYPE, "application/xml")
+ transformer.setOutputProperty(OutputKeys.METHOD, "xml")
+ transformer.setOutputProperty(OutputKeys.INDENT, "yes")
+ transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4")
+
+ val stringWriter = StringWriter()
+ transformer.transform(DOMSource(document), StreamResult(stringWriter))
+ return stringWriter.toString()
+ } catch (e: TransformerException) {
+ throw RuntimeException("Error transforming XML", e)
+ }
+ }
+
+ private fun saveFile(document: Document, fileName: String) {
+ val outPath = context.filesDir.path + "/" + fileName
+ Log.d("ColorSchemeXml", "Artifact $fileName created")
+ val writer = FileWriter(File(outPath))
+ writer.write(documentToString(document))
+ writer.close()
+ }
+}
+
+private fun Element.createColorEntry(name: String, value: Int, comment: String? = null) {
+ val doc = this.ownerDocument
+
+ if (comment != null) {
+ this.appendChild(doc.createComment(comment))
+ }
+
+ val color = doc.createElement("color")
+ this.appendChild(color)
+
+ color.setAttribute("name", name)
+ color.appendChild(doc.createTextNode("#" + value.toRGBHex()))
+}
+
+private fun Node.appendWithBreak(child: Node, lineBreaks: Int = 1): Node {
+ val doc = if (this is Document) this else this.ownerDocument
+ val node = doc.createTextNode("\n".repeat(lineBreaks))
+ this.appendChild(node)
+ return this.appendChild(child)
+}
+
+private fun Int.toRGBHex(): String {
+ return "%06X".format(0xFFFFFF and this)
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java
index ab28a2f..ed7c956 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java
@@ -985,7 +985,7 @@
FabricatedOverlay neutrals = overlays[1];
FabricatedOverlay dynamic = overlays[2];
- final int colorsPerPalette = 12;
+ final int colorsPerPalette = 13;
// Color resources were added for all 3 accent palettes
verify(accents, times(colorsPerPalette * 3))