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