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

Commit 7732fd24 authored by James O'Leary's avatar James O'Leary
Browse files

Add content color scheme for media

Use content color scheme for media player, which dynamically adapts
color palette chromas to the source color, and permits monochrome source
colors. Use for media. This allows ex. grayscale album covers to get a
grayscale treatment in UI, per design requirement.

Bug: 213314628
Test: Manual inspection at runtime
Change-Id: Ib8ea007e34758cd7903b3432ab88af695cb22f68
parent a3f6dae9
Loading
Loading
Loading
Loading
+39 −14
Original line number Diff line number Diff line
@@ -21,7 +21,6 @@ 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.max
import kotlin.math.roundToInt
@@ -143,12 +142,24 @@ internal class ChromaMinimum(val chroma: Double) : Chroma {
    }
}

internal class ChromaMultiple(val multiple: Double) : Chroma {
    override fun get(sourceColor: Cam): Double {
        return sourceColor.chroma * multiple
    }
}

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)
@@ -208,6 +219,13 @@ enum class Style(internal val coreSpec: CoreSpec) {
            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))
    )),
}

class ColorScheme(
@@ -231,7 +249,7 @@ class ColorScheme(
        darkTheme: Boolean,
        style: Style = Style.TONAL_SPOT
    ):
            this(getSeedColor(wallpaperColors), darkTheme, style)
            this(getSeedColor(wallpaperColors, style != Style.CONTENT), darkTheme, style)

    val allAccentColors: List<Int>
        get() {
@@ -260,7 +278,7 @@ class ColorScheme(
        val proposedSeedCam = Cam.fromInt(seed)
        val seedArgb = if (seed == Color.TRANSPARENT) {
            GOOGLE_BLUE
        } else if (proposedSeedCam.chroma < 5) {
        } else if (style != Style.CONTENT && proposedSeedCam.chroma < 5) {
            GOOGLE_BLUE
        } else {
            seed
@@ -289,22 +307,26 @@ class ColorScheme(
         * Identifies a color to create a color scheme from.
         *
         * @param wallpaperColors Colors extracted from an image via quantization.
         * @param filter If false, allow colors that have low chroma, creating grayscale themes.
         * @return ARGB int representing the color
         */
        @JvmStatic
        @JvmOverloads
        @ColorInt
        fun getSeedColor(wallpaperColors: WallpaperColors): Int {
            return getSeedColors(wallpaperColors).first()
        fun getSeedColor(wallpaperColors: WallpaperColors, filter: Boolean = true): Int {
            return getSeedColors(wallpaperColors, filter).first()
        }

        /**
         * Filters and ranks colors from WallpaperColors.
         *
         * @param wallpaperColors Colors extracted from an image via quantization.
         * @param filter If false, allow colors that have low chroma, creating grayscale themes.
         * @return List of ARGB ints, ordered from highest scoring to lowest.
         */
        @JvmStatic
        fun getSeedColors(wallpaperColors: WallpaperColors): List<Int> {
        @JvmOverloads
        fun getSeedColors(wallpaperColors: WallpaperColors, filter: Boolean = true): List<Int> {
            val totalPopulation = wallpaperColors.allColors.values.reduce { a, b -> a + b }
                    .toDouble()
            val totalPopulationMeaningless = (totalPopulation == 0.0)
@@ -317,9 +339,12 @@ class ColorScheme(
                val distinctColors = wallpaperColors.mainColors.map {
                    it.toArgb()
                }.distinct().filter {
                    if (!filter) {
                        true
                    } else {
                        Cam.fromInt(it).chroma >= MIN_CHROMA
                    }
                }.toList()

                if (distinctColors.isEmpty()) {
                    return listOf(GOOGLE_BLUE)
                }
@@ -332,7 +357,7 @@ class ColorScheme(
            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)
            val hueProportions = huePopulations(intToCam, intToProportion, filter)
            // Map each color to the percentage of the image with its hue.
            val intToHueProportion = wallpaperColors.allColors.mapValues {
                val cam = intToCam[it.key]!!
@@ -346,13 +371,12 @@ class ColorScheme(
            // 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 filteredIntToCam = if (!filter) intToCam else (intToCam.filter {
                val cam = it.value
                val lstar = lstarFromInt(it.key)
                val proportion = intToHueProportion[it.key]!!
                cam.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]!!)
@@ -444,7 +468,8 @@ class ColorScheme(

        private fun huePopulations(
            camByColor: Map<Int, Cam>,
            populationByColor: Map<Int, Double>
            populationByColor: Map<Int, Double>,
            filter: Boolean = true
        ): List<Double> {
            val huePopulation = List(size = 360, init = { 0.0 }).toMutableList()

@@ -452,7 +477,7 @@ class ColorScheme(
                val population = populationByColor[entry.key]!!
                val cam = camByColor[entry.key]!!
                val hue = cam.hue.roundToInt() % 360
                if (cam.chroma <= MIN_CHROMA) {
                if (filter && cam.chroma <= MIN_CHROMA) {
                    continue
                }
                huePopulation[hue] = huePopulation[hue] + population
+4 −2
Original line number Diff line number Diff line
@@ -75,6 +75,7 @@ import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.media.dialog.MediaOutputDialogFactory;
import com.android.systemui.monet.ColorScheme;
import com.android.systemui.monet.Style;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.plugins.FalsingManager;
import com.android.systemui.shared.system.SysUiStatsLog;
@@ -589,7 +590,7 @@ public class MediaControlPanel {
            if (artworkIcon != null) {
                WallpaperColors wallpaperColors = WallpaperColors
                        .fromBitmap(artworkIcon.getBitmap());
                mutableColorScheme = new ColorScheme(wallpaperColors, true);
                mutableColorScheme = new ColorScheme(wallpaperColors, true, Style.CONTENT);
                artwork = getScaledBackground(artworkIcon, width, height);
                isArtworkBound = true;
            } else {
@@ -599,7 +600,8 @@ public class MediaControlPanel {
                try {
                    Drawable icon = mContext.getPackageManager()
                            .getApplicationIcon(data.getPackageName());
                    mutableColorScheme = new ColorScheme(WallpaperColors.fromDrawable(icon), true);
                    mutableColorScheme = new ColorScheme(WallpaperColors.fromDrawable(icon), true,
                            Style.CONTENT);
                } catch (PackageManager.NameNotFoundException e) {
                    Log.w(TAG, "Cannot find icon for package " + data.getPackageName(), e);
                }
+9 −0
Original line number Diff line number Diff line
@@ -78,6 +78,7 @@ import org.json.JSONException;
import org.json.JSONObject;

import java.io.PrintWriter;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
@@ -639,6 +640,11 @@ public class ThemeOverlayController extends CoreStartable implements Dumpable {
    }

    private Style fetchThemeStyleFromSetting() {
        // Allow-list of Style objects that can be created from a setting string, i.e. can be
        // used as a system-wide theme.
        // - Content intentionally excluded, intended for media player, not system-wide
        List<Style> validStyles = Arrays.asList(Style.EXPRESSIVE, Style.SPRITZ, Style.TONAL_SPOT,
                Style.FRUIT_SALAD, Style.RAINBOW, Style.VIBRANT);
        Style style = mThemeStyle;
        final String overlayPackageJson = mSecureSettings.getStringForUser(
                Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES,
@@ -648,6 +654,9 @@ public class ThemeOverlayController extends CoreStartable implements Dumpable {
                JSONObject object = new JSONObject(overlayPackageJson);
                style = Style.valueOf(
                        object.getString(ThemeOverlayApplier.OVERLAY_CATEGORY_THEME_STYLE));
                if (!validStyles.contains(style)) {
                    style = Style.TONAL_SPOT;
                }
            } catch (JSONException | IllegalArgumentException e) {
                Log.i(TAG, "Failed to parse THEME_CUSTOMIZATION_OVERLAY_PACKAGES.", e);
                style = Style.TONAL_SPOT;
+5 −1
Original line number Diff line number Diff line
@@ -74,6 +74,8 @@ import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executor;

@@ -345,7 +347,9 @@ public class ThemeOverlayControllerTest extends SysuiTestCase {
    @Test
    public void onSettingChanged_honorThemeStyle() {
        when(mDeviceProvisionedController.isUserSetup(anyInt())).thenReturn(true);
        for (Style style : Style.values()) {
        List<Style> validStyles = Arrays.asList(Style.EXPRESSIVE, Style.SPRITZ, Style.TONAL_SPOT,
                Style.FRUIT_SALAD, Style.RAINBOW, Style.VIBRANT);
        for (Style style : validStyles) {
            reset(mSecureSettings);

            String jsonString = "{\"android.theme.customization.system_palette\":\"A16B00\","