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
This commit is contained in:
James O'Leary
2022-05-12 16:46:33 +00:00
parent a3f6dae96f
commit 7732fd2401
4 changed files with 57 additions and 17 deletions

View File

@@ -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 {
Cam.fromInt(it).chroma >= MIN_CHROMA
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

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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\","