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.Style; 29 import com.android.systemui.monet.TonalPalette; 30 import com.android.tools.layoutlib.annotations.VisibleForTesting; 31 32 import android.app.WallpaperColors; 33 import android.graphics.Bitmap; 34 import android.graphics.BitmapFactory; 35 import android.graphics.Color; 36 37 import java.io.IOException; 38 import java.io.InputStream; 39 import java.util.HashMap; 40 import java.util.List; 41 import java.util.Map; 42 43 /** 44 * Wrapper for RenderResources that allows overriding default system colors 45 * when using dynamic theming. 46 */ 47 public class DynamicRenderResources extends RenderResources { 48 private final RenderResources mBaseResources; 49 private Map<String, Integer> mDynamicColorMap; 50 DynamicRenderResources(RenderResources baseResources)51 public DynamicRenderResources(RenderResources baseResources) { 52 mBaseResources = baseResources; 53 } 54 55 @Override setLogger(ILayoutLog logger)56 public void setLogger(ILayoutLog logger) { 57 mBaseResources.setLogger(logger); 58 } 59 60 @Override getDefaultTheme()61 public StyleResourceValue getDefaultTheme() { 62 return mBaseResources.getDefaultTheme(); 63 } 64 65 @Override applyStyle(StyleResourceValue theme, boolean useAsPrimary)66 public void applyStyle(StyleResourceValue theme, boolean useAsPrimary) { 67 mBaseResources.applyStyle(theme, useAsPrimary); 68 } 69 70 @Override clearStyles()71 public void clearStyles() { 72 mBaseResources.clearStyles(); 73 } 74 75 @Override getAllThemes()76 public List<StyleResourceValue> getAllThemes() { 77 return mBaseResources.getAllThemes(); 78 } 79 80 @Override findItemInTheme(ResourceReference attr)81 public ResourceValue findItemInTheme(ResourceReference attr) { 82 ResourceValue baseValue = mBaseResources.findItemInTheme(attr); 83 return resolveDynamicColors(baseValue); 84 } 85 86 @Override findItemInStyle(StyleResourceValue style, ResourceReference attr)87 public ResourceValue findItemInStyle(StyleResourceValue style, ResourceReference attr) { 88 ResourceValue baseValue = mBaseResources.findItemInStyle(style, attr); 89 return resolveDynamicColors(baseValue); 90 } 91 92 @Override findResValue(String reference, boolean forceFrameworkOnly)93 public ResourceValue findResValue(String reference, boolean forceFrameworkOnly) { 94 ResourceValue baseValue = mBaseResources.findResValue(reference, forceFrameworkOnly); 95 return resolveDynamicColors(baseValue); 96 } 97 98 @Override dereference(ResourceValue resourceValue)99 public ResourceValue dereference(ResourceValue resourceValue) { 100 ResourceValue baseValue = mBaseResources.dereference(resourceValue); 101 return resolveDynamicColors(baseValue); 102 } 103 104 @Override getUnresolvedResource(ResourceReference reference)105 public ResourceValue getUnresolvedResource(ResourceReference reference) { 106 ResourceValue baseValue = mBaseResources.getUnresolvedResource(reference); 107 return resolveDynamicColors(baseValue); 108 } 109 110 @Override getResolvedResource(ResourceReference reference)111 public ResourceValue getResolvedResource(ResourceReference reference) { 112 ResourceValue baseValue = mBaseResources.getResolvedResource(reference); 113 return resolveDynamicColors(baseValue); 114 } 115 116 @Override resolveResValue(ResourceValue value)117 public ResourceValue resolveResValue(ResourceValue value) { 118 ResourceValue baseValue = mBaseResources.resolveResValue(value); 119 return resolveDynamicColors(baseValue); 120 } 121 122 @Override getParent(StyleResourceValue style)123 public StyleResourceValue getParent(StyleResourceValue style) { 124 return mBaseResources.getParent(style); 125 } 126 127 @Override getStyle(ResourceReference reference)128 public StyleResourceValue getStyle(ResourceReference reference) { 129 return mBaseResources.getStyle(reference); 130 } 131 resolveDynamicColors(ResourceValue baseValue)132 private ResourceValue resolveDynamicColors(ResourceValue baseValue) { 133 if (hasDynamicColors() && baseValue != null && isDynamicColor(baseValue)) { 134 int dynamicColor = mDynamicColorMap.get(baseValue.getName()); 135 String colorHex = "#" + Integer.toHexString(dynamicColor).substring(2); 136 return new ResourceValueImpl(baseValue.getNamespace(), baseValue.getResourceType(), 137 baseValue.getName(), colorHex); 138 } 139 return baseValue; 140 } 141 setWallpaper(String wallpaperPath, boolean isNightMode)142 public void setWallpaper(String wallpaperPath, boolean isNightMode) { 143 if (wallpaperPath == null) { 144 mDynamicColorMap = null; 145 return; 146 } 147 mDynamicColorMap = createDynamicColorMap(wallpaperPath, isNightMode); 148 } 149 150 /** 151 * Extracts colors from the wallpaper and creates the corresponding dynamic theme. 152 * It uses the main wallpaper color and the {@link Style#TONAL_SPOT} style. 153 * 154 * @param wallpaperPath path of the wallpaper resource to use 155 * @param isNightMode whether to use night mode or not 156 * 157 * @return map of system color names to their dynamic values 158 */ 159 @VisibleForTesting createDynamicColorMap(String wallpaperPath, boolean isNightMode)160 static Map<String, Integer> createDynamicColorMap(String wallpaperPath, boolean isNightMode) { 161 try (InputStream stream = DynamicRenderResources.class.getResourceAsStream(wallpaperPath)) { 162 Bitmap wallpaper = BitmapFactory.decodeStream(stream); 163 if (wallpaper == null) { 164 return null; 165 } 166 WallpaperColors wallpaperColors = WallpaperColors.fromBitmap(wallpaper); 167 int seed = ColorScheme.getSeedColor(wallpaperColors); 168 ColorScheme scheme = new ColorScheme(seed, isNightMode); 169 Map<String, Integer> dynamicColorMap = new HashMap<>(); 170 extractPalette("accent1", dynamicColorMap, scheme.getAccent1()); 171 extractPalette("accent2", dynamicColorMap, scheme.getAccent2()); 172 extractPalette("accent3", dynamicColorMap, scheme.getAccent3()); 173 extractPalette("neutral1", dynamicColorMap, scheme.getNeutral1()); 174 extractPalette("neutral2", dynamicColorMap, scheme.getNeutral2()); 175 return dynamicColorMap; 176 } catch (IllegalArgumentException | IOException ignore) { 177 return null; 178 } 179 } 180 181 /** 182 * Builds the dynamic theme from the {@link ColorScheme} copying what is done 183 * in {@link ThemeOverlayController#getOverlay} 184 */ extractPalette(String name, Map<String, Integer> colorMap, TonalPalette tonalPalette)185 private static void extractPalette(String name, 186 Map<String, Integer> colorMap, TonalPalette tonalPalette) { 187 String resourcePrefix = "system_" + name; 188 tonalPalette.allShadesMapped.forEach((key, value) -> { 189 String resourceName = resourcePrefix + "_" + key; 190 int colorValue = ColorUtils.setAlphaComponent(value, 0xFF); 191 colorMap.put(resourceName, colorValue); 192 }); 193 colorMap.put(resourcePrefix + "_0", Color.WHITE); 194 } 195 isDynamicColor(ResourceValue resourceValue)196 private static boolean isDynamicColor(ResourceValue resourceValue) { 197 if (!resourceValue.isFramework() || resourceValue.getResourceType() != ResourceType.COLOR) { 198 return false; 199 } 200 return resourceValue.getName().startsWith("system_accent") 201 || resourceValue.getName().startsWith("system_neutral"); 202 } 203 hasDynamicColors()204 public boolean hasDynamicColors() { 205 return mDynamicColorMap != null; 206 } 207 } 208