Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 5241833f authored by Hawkwood Glazier's avatar Hawkwood Glazier
Browse files

Refactor Clock theme handling

This change also removes some unnessecary parts of the copied version of
SimpleDigitalClockTextView and AssetLoader.

Bug: 364680879
Test: Presubmits & manually checked clock colors
Flag: com.android.systemui.clock_reactive_variants
Change-Id: I40c3f18277cdca920b76f878d9dc37305f40f696
parent 42cdae92
Loading
Loading
Loading
Loading
+1 −200
Original line number Diff line number Diff line
@@ -17,34 +17,24 @@
package com.android.systemui.shared.clocks

import android.content.Context
import android.content.res.ColorStateList
import android.content.res.Resources
import android.graphics.Color
import android.graphics.Typeface
import android.graphics.drawable.Drawable
import android.util.TypedValue
import com.android.internal.graphics.ColorUtils
import com.android.internal.graphics.cam.Cam
import com.android.internal.graphics.cam.CamUtils
import com.android.internal.policy.SystemBarUtils
import com.android.systemui.log.core.Logger
import com.android.systemui.log.core.MessageBuffer
import com.android.systemui.monet.ColorScheme
import com.android.systemui.monet.Style as MonetStyle
import com.android.systemui.monet.TonalPalette
import java.io.IOException
import kotlin.math.abs

class AssetLoader
private constructor(
    private val pluginCtx: Context,
    private val sysuiCtx: Context,
    private val baseDir: String,
    var colorScheme: ColorScheme?,
    var seedColor: Int?,
    var overrideChroma: Float?,
    val typefaceCache: TypefaceCache,
    val getThemeSeedColor: (Context) -> Int,
    messageBuffer: MessageBuffer,
) {
    val logger = Logger(messageBuffer, TAG)
@@ -59,12 +49,10 @@ private constructor(
        sysuiCtx: Context,
        baseDir: String,
        messageBuffer: MessageBuffer,
        getThemeSeedColor: ((Context) -> Int)? = null,
    ) : this(
        pluginCtx,
        sysuiCtx,
        baseDir,
        colorScheme = null,
        seedColor = null,
        overrideChroma = null,
        typefaceCache =
@@ -72,7 +60,6 @@ private constructor(
                // TODO(b/364680873): Move constant to config_clockFontFamily when shipping
                return@TypefaceCache Typeface.create("google-sans-flex-clock", Typeface.NORMAL)
            },
        getThemeSeedColor = getThemeSeedColor ?: Companion::getThemeSeedColor,
        messageBuffer = messageBuffer,
    )

@@ -92,107 +79,6 @@ private constructor(
        return res.getString(id)
    }

    fun tryReadColor(resStr: String): Int? = tryRead(resStr, ::readColor)

    fun readColor(resStr: String): Int {
        if (resStr.startsWith("#")) {
            return Color.parseColor(resStr)
        }

        val schemeColor = tryParseColorFromScheme(resStr)
        if (schemeColor != null) {
            logColor("ColorScheme: $resStr", schemeColor)
            return checkChroma(schemeColor)
        }

        val result = resolveColorResourceId(resStr)
        if (result == null) {
            throw IOException("Failed to parse color: $resStr")
        }

        val (res, colorId, targetTone) = result
        val color = res.getColor(colorId)
        if (targetTone == null || TonalPalette.SHADE_KEYS.contains(targetTone.toInt())) {
            logColor("Resources: $resStr", color)
            return checkChroma(color)
        } else {
            val interpolatedColor =
                ColorStateList.valueOf(color)
                    .withLStar((1000f - targetTone) / 10f)
                    .getDefaultColor()
            logColor("Resources (interpolated tone): $resStr", interpolatedColor)
            return checkChroma(interpolatedColor)
        }
    }

    private fun checkChroma(color: Int): Int {
        return overrideChroma?.let {
            val cam = Cam.fromInt(color)
            val tone = CamUtils.lstarFromInt(color)
            val result = ColorUtils.CAMToColor(cam.hue, it, tone)
            logColor("Chroma override", result)
            result
        } ?: color
    }

    private fun tryParseColorFromScheme(resStr: String): Int? {
        val colorScheme = this.colorScheme
        if (colorScheme == null) {
            logger.w("No color scheme available")
            return null
        }

        val (packageName, category, name) = parseResourceId(resStr)
        if (packageName != "android" || category != "color") {
            logger.w("Failed to parse package from $resStr")
            return null
        }

        var parts = name.split('_')
        if (parts.size != 3) {
            logger.w("Failed to find palette and shade from $name")
            return null
        }
        val (_, paletteKey, shadeKeyStr) = parts

        val palette =
            when (paletteKey) {
                "accent1" -> colorScheme.accent1
                "accent2" -> colorScheme.accent2
                "accent3" -> colorScheme.accent3
                "neutral1" -> colorScheme.neutral1
                "neutral2" -> colorScheme.neutral2
                else -> return null
            }

        if (shadeKeyStr.contains("+") || shadeKeyStr.contains("-")) {
            val signIndex = shadeKeyStr.indexOfLast { it == '-' || it == '+' }
            // Use the tone of the seed color if it was set explicitly.
            var baseTone =
                if (seedColor != null) colorScheme.seedTone.toFloat()
                else shadeKeyStr.substring(0, signIndex).toFloatOrNull()
            val diff = shadeKeyStr.substring(signIndex).toFloatOrNull()

            if (baseTone == null) {
                logger.w("Failed to parse base tone from $shadeKeyStr")
                return null
            }

            if (diff == null) {
                logger.w("Failed to parse relative tone from $shadeKeyStr")
                return null
            }
            return palette.getAtTone(baseTone + diff)
        } else {
            val shadeKey = shadeKeyStr.toIntOrNull()
            if (shadeKey == null) {
                logger.w("Failed to parse tone from $shadeKeyStr")
                return null
            }
            return palette.allShadesMapped.get(shadeKey) ?: palette.getAtTone(shadeKey.toFloat())
        }
    }

    fun readFontAsset(resStr: String): Typeface = typefaceCache.getTypeface(resStr)

    fun tryReadTextAsset(path: String?): String? = tryRead(path, ::readTextAsset)
@@ -250,52 +136,6 @@ private constructor(
        }
    }

    fun resolveColorResourceId(resStr: String): Triple<Resources, Int, Float?>? {
        var (packageName, category, name) = parseResourceId(resStr)

        // Convert relative tonal specifiers to standard
        val relIndex = name.indexOfLast { it == '_' }
        val isToneRelative = name.contains("-") || name.contains("+")
        val targetTone =
            if (packageName != "android") {
                null
            } else if (isToneRelative) {
                val signIndex = name.indexOfLast { it == '-' || it == '+' }
                val baseTone = name.substring(relIndex + 1, signIndex).toFloatOrNull()
                var diff = name.substring(signIndex).toFloatOrNull()
                if (baseTone == null || diff == null) {
                    logger.w("Failed to parse relative tone from $name")
                    return null
                }
                baseTone + diff
            } else {
                val absTone = name.substring(relIndex + 1).toFloatOrNull()
                if (absTone == null) {
                    logger.w("Failed to parse absolute tone from $name")
                    return null
                }
                absTone
            }

        if (
            targetTone != null &&
                (isToneRelative || !TonalPalette.SHADE_KEYS.contains(targetTone.toInt()))
        ) {
            val closeTone = TonalPalette.SHADE_KEYS.minBy { abs(it - targetTone) }
            val prevName = name
            name = name.substring(0, relIndex + 1) + closeTone
            logger.i("Converted $prevName to $name")
        }

        val result = resolveResourceId(packageName, category, name)
        if (result == null) {
            return null
        }

        val (res, resId) = result
        return Triple(res, resId, targetTone)
    }

    fun resolveResourceId(resStr: String): Pair<Resources, Int>? {
        val (packageName, category, name) = parseResourceId(resStr)
        return resolveResourceId(packageName, category, name)
@@ -331,8 +171,7 @@ private constructor(
        try {
            if (path.startsWith("@")) {
                val pair = resolveResourceId(path)
                val colorPair = resolveColorResourceId(path)
                return pair != null || colorPair != null
                return pair != null
            } else {
                val stream = pluginCtx.resources.assets.open("$baseDir$path")
                if (stream == null) {
@@ -352,37 +191,14 @@ private constructor(
            pluginCtx,
            sysuiCtx,
            baseDir,
            colorScheme,
            seedColor,
            overrideChroma,
            typefaceCache,
            getThemeSeedColor,
            messageBuffer ?: logger.buffer,
        )

    fun setSeedColor(seedColor: Int?, style: MonetStyle?) {
        this.seedColor = seedColor
        refreshColorPalette(style)
    }

    fun refreshColorPalette(style: MonetStyle?) {
        val seedColor =
            this.seedColor ?: getThemeSeedColor(sysuiCtx).also { logColor("Theme Seed Color", it) }
        this.colorScheme =
            ColorScheme(
                seedColor,
                false, // darkTheme is not used for palette generation
                style ?: MonetStyle.CLOCK,
            )

        // Enforce low chroma on output colors if low chroma theme is selected
        this.overrideChroma = run {
            val cam = colorScheme?.seed?.let { Cam.fromInt(it) }
            if (cam != null && cam.chroma < LOW_CHROMA_LIMIT) {
                return@run cam.chroma * LOW_CHROMA_SCALE
            }
            return@run null
        }
    }

    fun getClockPaddingStart(): Int {
@@ -430,22 +246,7 @@ private constructor(
        throw Exception("Cannot find id of $name from $TAG")
    }

    private fun logColor(name: String, color: Int) {
        if (DEBUG_COLOR) {
            val cam = Cam.fromInt(color)
            val tone = CamUtils.lstarFromInt(color)
            logger.i("$name -> (hue: ${cam.hue}, chroma: ${cam.chroma}, tone: $tone)")
        }
    }

    companion object {
        private val DEBUG_COLOR = true
        private val LOW_CHROMA_LIMIT = 15
        private val LOW_CHROMA_SCALE = 1.5f
        private val TAG = AssetLoader::class.simpleName!!

        private fun getThemeSeedColor(ctx: Context): Int {
            return ctx.resources.getColor(android.R.color.system_palette_key_color_primary_light)
        }
    }
}
+10 −12
Original line number Diff line number Diff line
@@ -28,6 +28,7 @@ import com.android.systemui.plugins.clocks.ClockEvents
import com.android.systemui.plugins.clocks.ClockFaceConfig
import com.android.systemui.plugins.clocks.ClockFaceEvents
import com.android.systemui.plugins.clocks.ClockReactiveSetting
import com.android.systemui.plugins.clocks.ThemeConfig
import com.android.systemui.plugins.clocks.WeatherData
import com.android.systemui.plugins.clocks.ZenData
import com.android.systemui.shared.clocks.view.FlexClockView
@@ -46,7 +47,6 @@ class ComposedDigitalLayerController(

    val layerControllers = mutableListOf<SimpleClockLayerController>()
    val dozeState = DefaultClockController.AnimationState(1F)
    var isRegionDark = true

    override val view = FlexClockView(ctx, assets, messageBuffer)

@@ -103,10 +103,6 @@ class ComposedDigitalLayerController(
                view.onZenDataChanged(data)
            }

            override fun onColorPaletteChanged(resources: Resources) {}

            override fun onSeedColorChanged(seedColor: Int?) {}

            override fun onReactiveAxesChanged(axes: List<ClockReactiveSetting>) {}

            override var isReactiveTouchInteractionEnabled
@@ -116,10 +112,6 @@ class ComposedDigitalLayerController(
                }
        }

    override fun updateColors() {
        view.updateColors(assets, isRegionDark)
    }

    override val animations =
        object : ClockAnimations {
            override fun enter() {
@@ -158,9 +150,15 @@ class ComposedDigitalLayerController(
                refreshTime()
            }

            override fun onRegionDarknessChanged(isRegionDark: Boolean) {
                this@ComposedDigitalLayerController.isRegionDark = isRegionDark
                updateColors()
            override fun onThemeChanged(theme: ThemeConfig) {
                val color =
                    when {
                        theme.seedColor != null -> theme.seedColor!!
                        theme.isDarkTheme -> resources.getColor(android.R.color.system_accent1_100)
                        else -> resources.getColor(android.R.color.system_accent2_600)
                    }

                view.updateColor(color)
            }

            override fun onFontSettingChanged(fontSizePx: Float) {
+32 −46
Original line number Diff line number Diff line
@@ -37,6 +37,7 @@ import com.android.systemui.plugins.clocks.ClockMessageBuffers
import com.android.systemui.plugins.clocks.ClockReactiveSetting
import com.android.systemui.plugins.clocks.ClockSettings
import com.android.systemui.plugins.clocks.DefaultClockFaceLayout
import com.android.systemui.plugins.clocks.ThemeConfig
import com.android.systemui.plugins.clocks.WeatherData
import com.android.systemui.plugins.clocks.ZenData
import java.io.PrintWriter
@@ -100,28 +101,33 @@ class DefaultClockController(
        events.onLocaleChanged(Locale.getDefault())
    }

    override fun initialize(resources: Resources, dozeFraction: Float, foldFraction: Float) {
    override fun initialize(isDarkTheme: Boolean, dozeFraction: Float, foldFraction: Float) {
        largeClock.recomputePadding(null)

        largeClock.animations = LargeClockAnimations(largeClock.view, dozeFraction, foldFraction)
        smallClock.animations = DefaultClockAnimations(smallClock.view, dozeFraction, foldFraction)
        events.onColorPaletteChanged(resources)

        val theme = ThemeConfig(isDarkTheme, settings?.seedColor)
        largeClock.events.onThemeChanged(theme)
        smallClock.events.onThemeChanged(theme)

        events.onTimeZoneChanged(TimeZone.getDefault())

        smallClock.events.onTimeTick()
        largeClock.events.onTimeTick()
    }

    open inner class DefaultClockFaceController(
        override val view: AnimatableClockView,
        var seedColor: Int?,
        seedColor: Int?,
        messageBuffer: MessageBuffer?,
    ) : ClockFaceController {

        // MAGENTA is a placeholder, and will be assigned correctly in initialize
        private var currentColor = Color.MAGENTA
        private var isRegionDark = false
        private var currentColor = seedColor ?: Color.MAGENTA
        protected var targetRegion: Rect? = null

        override val config = ClockFaceConfig()
        override var theme = ThemeConfig(true, seedColor)
        override val layout =
            DefaultClockFaceLayout(view).apply {
                views[0].id =
@@ -132,9 +138,6 @@ class DefaultClockController(
            internal set

        init {
            if (seedColor != null) {
                currentColor = seedColor!!
            }
            view.setColors(DOZE_COLOR, currentColor)
            messageBuffer?.let { view.messageBuffer = it }
        }
@@ -143,9 +146,26 @@ class DefaultClockController(
            object : ClockFaceEvents {
                override fun onTimeTick() = view.refreshTime()

                override fun onRegionDarknessChanged(isRegionDark: Boolean) {
                    this@DefaultClockFaceController.isRegionDark = isRegionDark
                    updateColor()
                override fun onThemeChanged(theme: ThemeConfig) {
                    this@DefaultClockFaceController.theme = theme

                    val color =
                        when {
                            theme.seedColor != null -> theme.seedColor!!
                            theme.isDarkTheme ->
                                resources.getColor(android.R.color.system_accent1_100)
                            else -> resources.getColor(android.R.color.system_accent2_600)
                        }

                    if (currentColor == color) {
                        return
                    }

                    currentColor = color
                    view.setColors(DOZE_COLOR, color)
                    if (!animations.dozeState.isActive) {
                        view.animateColorChange()
                    }
                }

                override fun onTargetRegionChanged(targetRegion: Rect?) {
@@ -165,27 +185,6 @@ class DefaultClockController(
            }

        open fun recomputePadding(targetRegion: Rect?) {}

        fun updateColor() {
            val color =
                if (seedColor != null) {
                    seedColor!!
                } else if (isRegionDark) {
                    resources.getColor(android.R.color.system_accent1_100)
                } else {
                    resources.getColor(android.R.color.system_accent2_600)
                }

            if (currentColor == color) {
                return
            }

            currentColor = color
            view.setColors(DOZE_COLOR, color)
            if (!animations.dozeState.isActive) {
                view.animateColorChange()
            }
        }
    }

    inner class LargeClockFaceController(
@@ -248,19 +247,6 @@ class DefaultClockController(
        override fun onTimeZoneChanged(timeZone: TimeZone) =
            clocks.forEach { it.onTimeZoneChanged(timeZone) }

        override fun onColorPaletteChanged(resources: Resources) {
            largeClock.updateColor()
            smallClock.updateColor()
        }

        override fun onSeedColorChanged(seedColor: Int?) {
            largeClock.seedColor = seedColor
            smallClock.seedColor = seedColor

            largeClock.updateColor()
            smallClock.updateColor()
        }

        override fun onLocaleChanged(locale: Locale) {
            val nf = NumberFormat.getInstance(locale)
            if (nf.format(FORMAT_NUMBER.toLong()) == burmeseNumerals) {
+1 −0
Original line number Diff line number Diff line
@@ -58,6 +58,7 @@ class DefaultClockProvider(
            val buffer =
                messageBuffers?.infraMessageBuffer ?: LogcatOnlyMessageBuffer(LogLevel.INFO)
            val assets = AssetLoader(ctx, ctx, "clocks/", buffer)
            assets.setSeedColor(settings.seedColor, null)
            FlexClockController(ctx, resources, assets, FLEX_DESIGN, messageBuffers)
        } else {
            DefaultClockController(
+16 −26
Original line number Diff line number Diff line
@@ -25,6 +25,7 @@ import com.android.systemui.plugins.clocks.ClockController
import com.android.systemui.plugins.clocks.ClockEvents
import com.android.systemui.plugins.clocks.ClockMessageBuffers
import com.android.systemui.plugins.clocks.ClockReactiveSetting
import com.android.systemui.plugins.clocks.ThemeConfig
import com.android.systemui.plugins.clocks.WeatherData
import com.android.systemui.plugins.clocks.ZenData
import com.android.systemui.shared.clocks.view.FlexClockView
@@ -97,24 +98,6 @@ class FlexClockController(
                largeClock.events.onLocaleChanged(locale)
            }

            override fun onColorPaletteChanged(resources: Resources) {
                assets.refreshColorPalette(design.colorPalette)
                smallClock.assets.refreshColorPalette(design.colorPalette)
                largeClock.assets.refreshColorPalette(design.colorPalette)

                smallClock.events.onColorPaletteChanged(resources)
                largeClock.events.onColorPaletteChanged(resources)
            }

            override fun onSeedColorChanged(seedColor: Int?) {
                assets.setSeedColor(seedColor, design.colorPalette)
                smallClock.assets.setSeedColor(seedColor, design.colorPalette)
                largeClock.assets.setSeedColor(seedColor, design.colorPalette)

                smallClock.events.onSeedColorChanged(seedColor)
                largeClock.events.onSeedColorChanged(seedColor)
            }

            override fun onWeatherDataChanged(data: WeatherData) {
                smallClock.events.onWeatherDataChanged(data)
                largeClock.events.onWeatherDataChanged(data)
@@ -136,14 +119,21 @@ class FlexClockController(
            }
        }

    override fun initialize(resources: Resources, dozeFraction: Float, foldFraction: Float) {
        events.onColorPaletteChanged(resources)
        smallClock.animations.doze(dozeFraction)
        largeClock.animations.doze(dozeFraction)
        smallClock.animations.fold(foldFraction)
        largeClock.animations.fold(foldFraction)
        smallClock.events.onTimeTick()
        largeClock.events.onTimeTick()
    override fun initialize(isDarkTheme: Boolean, dozeFraction: Float, foldFraction: Float) {
        val theme = ThemeConfig(isDarkTheme, assets.seedColor)
        smallClock.run {
            events.onThemeChanged(theme)
            animations.doze(dozeFraction)
            animations.fold(foldFraction)
            events.onTimeTick()
        }

        largeClock.run {
            events.onThemeChanged(theme)
            animations.doze(dozeFraction)
            animations.fold(foldFraction)
            events.onTimeTick()
        }
    }

    override fun dump(pw: PrintWriter) {}
Loading