1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.layoutlib.bridge.android; 18 19 import com.android.ide.common.rendering.api.ILayoutLog; 20 import com.android.ide.common.rendering.api.RenderResources; 21 import com.android.ide.common.rendering.api.ResourceReference; 22 import com.android.ide.common.rendering.api.ResourceValue; 23 import com.android.ide.common.rendering.api.ResourceValueImpl; 24 import com.android.ide.common.rendering.api.StyleResourceValue; 25 import com.android.internal.graphics.ColorUtils; 26 import com.android.resources.ResourceType; 27 import com.android.systemui.monet.ColorScheme; 28 import com.android.systemui.monet.DynamicColors; 29 import com.android.systemui.monet.Style; 30 import com.android.systemui.monet.TonalPalette; 31 import com.android.tools.layoutlib.annotations.VisibleForTesting; 32 33 import android.app.WallpaperColors; 34 import android.graphics.Bitmap; 35 import android.graphics.BitmapFactory; 36 import android.graphics.Color; 37 import android.util.Pair; 38 39 import java.io.IOException; 40 import java.io.InputStream; 41 import java.util.HashMap; 42 import java.util.List; 43 import java.util.Map; 44 45 import com.google.ux.material.libmonet.dynamiccolor.DynamicColor; 46 47 /** 48 * Wrapper for RenderResources that allows overriding default system colors 49 * when using dynamic theming. 50 */ 51 public class DynamicRenderResources extends RenderResources { 52 private final RenderResources mBaseResources; 53 private Map<String, Integer> mDynamicColorMap; 54 DynamicRenderResources(RenderResources baseResources)55 public DynamicRenderResources(RenderResources baseResources) { 56 mBaseResources = baseResources; 57 } 58 59 @Override setLogger(ILayoutLog logger)60 public void setLogger(ILayoutLog logger) { 61 mBaseResources.setLogger(logger); 62 } 63 64 @Override getDefaultTheme()65 public StyleResourceValue getDefaultTheme() { 66 return mBaseResources.getDefaultTheme(); 67 } 68 69 @Override applyStyle(StyleResourceValue theme, boolean useAsPrimary)70 public void applyStyle(StyleResourceValue theme, boolean useAsPrimary) { 71 mBaseResources.applyStyle(theme, useAsPrimary); 72 } 73 74 @Override clearStyles()75 public void clearStyles() { 76 mBaseResources.clearStyles(); 77 } 78 79 @Override getAllThemes()80 public List<StyleResourceValue> getAllThemes() { 81 return mBaseResources.getAllThemes(); 82 } 83 84 @Override findItemInTheme(ResourceReference attr)85 public ResourceValue findItemInTheme(ResourceReference attr) { 86 ResourceValue baseValue = mBaseResources.findItemInTheme(attr); 87 return resolveDynamicColors(baseValue); 88 } 89 90 @Override findItemInStyle(StyleResourceValue style, ResourceReference attr)91 public ResourceValue findItemInStyle(StyleResourceValue style, ResourceReference attr) { 92 ResourceValue baseValue = mBaseResources.findItemInStyle(style, attr); 93 return resolveDynamicColors(baseValue); 94 } 95 96 @Override findResValue(String reference, boolean forceFrameworkOnly)97 public ResourceValue findResValue(String reference, boolean forceFrameworkOnly) { 98 ResourceValue baseValue = mBaseResources.findResValue(reference, forceFrameworkOnly); 99 return resolveDynamicColors(baseValue); 100 } 101 102 @Override dereference(ResourceValue resourceValue)103 public ResourceValue dereference(ResourceValue resourceValue) { 104 ResourceValue baseValue = mBaseResources.dereference(resourceValue); 105 return resolveDynamicColors(baseValue); 106 } 107 108 @Override getUnresolvedResource(ResourceReference reference)109 public ResourceValue getUnresolvedResource(ResourceReference reference) { 110 ResourceValue baseValue = mBaseResources.getUnresolvedResource(reference); 111 return resolveDynamicColors(baseValue); 112 } 113 114 @Override getResolvedResource(ResourceReference reference)115 public ResourceValue getResolvedResource(ResourceReference reference) { 116 ResourceValue baseValue = mBaseResources.getResolvedResource(reference); 117 return resolveDynamicColors(baseValue); 118 } 119 120 @Override resolveResValue(ResourceValue value)121 public ResourceValue resolveResValue(ResourceValue value) { 122 ResourceValue baseValue = mBaseResources.resolveResValue(value); 123 return resolveDynamicColors(baseValue); 124 } 125 126 @Override getParent(StyleResourceValue style)127 public StyleResourceValue getParent(StyleResourceValue style) { 128 return mBaseResources.getParent(style); 129 } 130 131 @Override getStyle(ResourceReference reference)132 public StyleResourceValue getStyle(ResourceReference reference) { 133 return mBaseResources.getStyle(reference); 134 } 135 resolveDynamicColors(ResourceValue baseValue)136 private ResourceValue resolveDynamicColors(ResourceValue baseValue) { 137 if (hasDynamicColors() && baseValue != null && isDynamicColor(baseValue)) { 138 int dynamicColor = mDynamicColorMap.get(baseValue.getName()); 139 String colorHex = "#" + Integer.toHexString(dynamicColor).substring(2); 140 return new ResourceValueImpl(baseValue.getNamespace(), baseValue.getResourceType(), 141 baseValue.getName(), colorHex); 142 } 143 return baseValue; 144 } 145 setWallpaper(String wallpaperPath, boolean isNightMode)146 public void setWallpaper(String wallpaperPath, boolean isNightMode) { 147 if (wallpaperPath == null) { 148 mDynamicColorMap = null; 149 return; 150 } 151 mDynamicColorMap = createDynamicColorMap(wallpaperPath, isNightMode); 152 } 153 154 /** 155 * Extracts colors from the wallpaper and creates the corresponding dynamic theme. 156 * It uses the main wallpaper color and the {@link Style#TONAL_SPOT} style. 157 * 158 * @param wallpaperPath path of the wallpaper resource to use 159 * @param isNightMode whether to use night mode or not 160 * 161 * @return map of system color names to their dynamic values 162 */ 163 @VisibleForTesting createDynamicColorMap(String wallpaperPath, boolean isNightMode)164 static Map<String, Integer> createDynamicColorMap(String wallpaperPath, boolean isNightMode) { 165 try (InputStream stream = DynamicRenderResources.class.getResourceAsStream(wallpaperPath)) { 166 Bitmap wallpaper = BitmapFactory.decodeStream(stream); 167 if (wallpaper == null) { 168 return null; 169 } 170 WallpaperColors wallpaperColors = WallpaperColors.fromBitmap(wallpaper); 171 int seed = ColorScheme.getSeedColor(wallpaperColors); 172 ColorScheme lightScheme = new ColorScheme(seed, false); 173 ColorScheme darkScheme = new ColorScheme(seed, true); 174 ColorScheme currentScheme = isNightMode ? darkScheme : lightScheme; 175 Map<String, Integer> dynamicColorMap = new HashMap<>(); 176 extractPalette("accent1", dynamicColorMap, currentScheme.getAccent1()); 177 extractPalette("accent2", dynamicColorMap, currentScheme.getAccent2()); 178 extractPalette("accent3", dynamicColorMap, currentScheme.getAccent3()); 179 extractPalette("neutral1", dynamicColorMap, currentScheme.getNeutral1()); 180 extractPalette("neutral2", dynamicColorMap, currentScheme.getNeutral2()); 181 182 //Themed Colors 183 extractDynamicColors(dynamicColorMap, lightScheme, darkScheme, 184 DynamicColors.getAllDynamicColorsMapped(false), false); 185 // Fixed Colors 186 extractDynamicColors(dynamicColorMap, lightScheme, darkScheme, 187 DynamicColors.getFixedColorsMapped(false), true); 188 //Custom Colors 189 extractDynamicColors(dynamicColorMap, lightScheme, darkScheme, 190 DynamicColors.getCustomColorsMapped(false), false); 191 return dynamicColorMap; 192 } catch (IllegalArgumentException | IOException ignore) { 193 return null; 194 } 195 } 196 197 /** 198 * Builds the dynamic theme from the {@link ColorScheme} copying what is done 199 * in {@link ThemeOverlayController#getOverlay} 200 */ extractPalette(String name, Map<String, Integer> colorMap, TonalPalette tonalPalette)201 private static void extractPalette(String name, 202 Map<String, Integer> colorMap, TonalPalette tonalPalette) { 203 String resourcePrefix = "system_" + name; 204 tonalPalette.allShadesMapped.forEach((key, value) -> { 205 String resourceName = resourcePrefix + "_" + key; 206 int colorValue = ColorUtils.setAlphaComponent(value, 0xFF); 207 colorMap.put(resourceName, colorValue); 208 }); 209 colorMap.put(resourcePrefix + "_0", Color.WHITE); 210 } 211 extractDynamicColors(Map<String, Integer> colorMap, ColorScheme lightScheme, ColorScheme darkScheme, List<Pair<String, DynamicColor>> colors, Boolean isFixed)212 private static void extractDynamicColors(Map<String, Integer> colorMap, ColorScheme lightScheme, 213 ColorScheme darkScheme, List<Pair<String, DynamicColor>> colors, Boolean isFixed) { 214 colors.forEach(p -> { 215 String prefix = "system_" + p.first; 216 217 if (isFixed) { 218 colorMap.put(prefix, p.second.getArgb(lightScheme.getMaterialScheme())); 219 return; 220 } 221 222 colorMap.put(prefix + "_light", p.second.getArgb(lightScheme.getMaterialScheme())); 223 colorMap.put(prefix + "_dark", p.second.getArgb(darkScheme.getMaterialScheme())); 224 }); 225 } 226 isDynamicColor(ResourceValue resourceValue)227 private boolean isDynamicColor(ResourceValue resourceValue) { 228 if (!resourceValue.isFramework() || resourceValue.getResourceType() != ResourceType.COLOR) { 229 return false; 230 } 231 return mDynamicColorMap.containsKey(resourceValue.getName()); 232 } 233 hasDynamicColors()234 public boolean hasDynamicColors() { 235 return mDynamicColorMap != null; 236 } 237 } 238