1 /*
2  * Copyright (C) 2021 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.managedprovisioning.common;
18 
19 import static java.util.Objects.requireNonNull;
20 
21 import androidx.annotation.ColorRes;
22 import androidx.appcompat.app.AppCompatDelegate;
23 
24 import com.android.managedprovisioning.R;
25 
26 import java.util.HashMap;
27 import java.util.Map;
28 
29 /**
30  * Helper which handles animation dynamic colors.
31  *
32  * <p>Animations adapt according to the system theme. The colors of specific layers are updated
33  * according to a color map.
34  *
35  * <p>{@link AnimationDynamicColorsHelper#setupAnimationDynamicColors(
36  * AnimationDynamicColorsHelper.AnimationWrapper, int)} does the following:
37  * <ol>
38  *     <li>Iterates through a predefined map of
39  *     light-dark mode layer names and colors (or vice-versa if device is in dark-mode)
40  *     and sets each layer to the appropriate color</li>
41  *     <li>Not all colors map bi-directionally, some colors are asymmetric
42  *     as several light colors can map to a single dark color. For these colors the method
43  *     iterates through a predefined map of light->dark mode layer names, and <i>only</i>
44  *     sets the light mode layers to the appropriate dark color. If the system is in light
45  *     mode, the method does nothing for this case.</li>
46  * </ol>
47  *
48  */
49 public class AnimationDynamicColorsHelper {
50     private static final HomogenousBiMap<ColorItem> sLightToDarkColorsBiMap =
51             buildLightToDarkColorsBiMap();
52 
53     /**
54      * A map which only maps colors from light to dark mode.
55      *
56      * <p>This is necessary in cases when multiple colors in light mode map to a single color to
57      * dark mode.
58      */
59     private static final Map<ColorItem, ColorItem> sLightToDarkOnly = buildLightToDarkColorsMap();
60 
61     /**
62      * A wrapper over dynamically-colored animations.
63      */
64     public interface AnimationWrapper {
65 
66         /**
67          * Loads the animation asynchronously. Must invoke {@code callback} when done.
68          */
loadAnimation(Runnable callback)69         void loadAnimation(Runnable callback);
70         /**
71          * Sets this animation's {@code layerName} layer to color with resource id
72          * {@code targetColorRes}.
73          */
setLayerColor(String layerName, @ColorRes int targetColorRes)74         void setLayerColor(String layerName, @ColorRes int targetColorRes);
75 
76     }
77     /**
78      * Updates the relevant animation with theme-specific colors.
79      *
80      * <p>The algorithm is:
81      * <ol>
82      *     <li>Iterates through a predefined map of
83      *     light-dark mode layer names and colors (or vice-versa if device is in dark-mode)
84      *     and sets each layer to the appropriate color</li>
85      *     <li>Not all colors map bi-directionally, some colors are asymmetric
86      *     as several light colors can map to a single dark color. For these colors the method
87      *     iterates through a predefined map of light->dark mode layer names, and <i>only</i>
88      *     sets the light mode layers to the appropriate dark color. If the system is in light
89      *     mode, the method does nothing for this case.</li>
90      * </ol>
91      *
92      * @param animationWrapper - a wrapper over the animation that supports dynamic colors
93      * @param nightMode - {@link AppCompatDelegate#MODE_NIGHT_NO} or {@link
94      * AppCompatDelegate#MODE_NIGHT_YES}
95      */
setupAnimationDynamicColors(AnimationWrapper animationWrapper, int nightMode)96     public void setupAnimationDynamicColors(AnimationWrapper animationWrapper, int nightMode) {
97         animationWrapper.loadAnimation(
98                 () -> setupAnimationDynamicColorsInternal(animationWrapper, nightMode));
99     }
100 
setupAnimationDynamicColorsInternal( AnimationWrapper animationWrapper, int nightMode)101     private void setupAnimationDynamicColorsInternal(
102             AnimationWrapper animationWrapper, int nightMode) {
103         setupSymmetricLayerColors(animationWrapper, nightMode);
104         setupLightToDarkLayerColors(animationWrapper, nightMode);
105     }
106 
setupLightToDarkLayerColors(AnimationWrapper animationWrapper, int nightMode)107     private void setupLightToDarkLayerColors(AnimationWrapper animationWrapper, int nightMode) {
108         if (nightMode == AppCompatDelegate.MODE_NIGHT_NO) {
109             return;
110         }
111         for (ColorItem lightColorItem : sLightToDarkOnly.keySet()) {
112             ColorItem darkColorItem = sLightToDarkOnly.get(lightColorItem);
113             animationWrapper.setLayerColor(lightColorItem.mLayerName, darkColorItem.mColorRes);
114         }
115     }
116 
setupSymmetricLayerColors(AnimationWrapper animationWrapper, int nightMode)117     private void setupSymmetricLayerColors(AnimationWrapper animationWrapper, int nightMode) {
118         Map<ColorItem, ColorItem> colorMap = getColorMap(nightMode);
119         for (ColorItem targetColorItem : colorMap.keySet()) {
120             ColorItem otherColorItem = colorMap.get(targetColorItem);
121             animationWrapper.setLayerColor(otherColorItem.mLayerName, targetColorItem.mColorRes);
122         }
123     }
124 
getColorMap(int nightMode)125     private Map<ColorItem, ColorItem> getColorMap(int nightMode) {
126         Map<ColorItem, ColorItem> map;
127         if (isDayMode(nightMode)) {
128             map = sLightToDarkColorsBiMap.getForwardMap();
129         } else {
130             map = sLightToDarkColorsBiMap.getReverseMap();
131         }
132         return map;
133     }
134 
isDayMode(int nightMode)135     private boolean isDayMode(int nightMode) {
136         return nightMode == AppCompatDelegate.MODE_NIGHT_NO;
137     }
138 
139     private static class ColorItem {
140 
141         private final int mColorRes;
142         private final String mLayerName;
ColorItem(int colorRes, String layerName)143         ColorItem(int colorRes, String layerName) {
144             this.mColorRes = colorRes;
145             this.mLayerName = requireNonNull(layerName);
146         }
147 
148     }
149 
buildLightToDarkColorsBiMap()150     private static HomogenousBiMap<ColorItem> buildLightToDarkColorsBiMap() {
151         HomogenousBiMap<ColorItem> result = new HomogenousBiMap<>();
152         result.put(
153                 new ColorItem(R.color.blue600, ".blue600"),
154                 new ColorItem(R.color.blue400, ".blue400"));
155         result.put(
156                 new ColorItem(R.color.green600, ".green600"),
157                 new ColorItem(R.color.green400, ".green400"));
158         result.put(
159                 new ColorItem(R.color.red600, ".red600"),
160                 new ColorItem(R.color.red400, ".red400"));
161         result.put(
162                 new ColorItem(R.color.yellow600, ".yellow600"),
163                 new ColorItem(R.color.yellow400, ".yellow400"));
164         result.put(
165                 new ColorItem(R.color.blue400, ".blue400"),
166                 new ColorItem(R.color.blue100, ".blue100"));
167         result.put(
168                 new ColorItem(R.color.green400, ".green400"),
169                 new ColorItem(R.color.green100, ".green100"));
170         result.put(
171                 new ColorItem(R.color.red400, ".red400"),
172                 new ColorItem(R.color.red100, ".red100"));
173         result.put(
174                 new ColorItem(R.color.yellow400, ".yellow400"),
175                 new ColorItem(R.color.yellow100, ".yellow100"));
176         result.put(
177                 new ColorItem(R.color.blue300, ".blue300"),
178                 new ColorItem(R.color.blue800, ".blue800"));
179         result.put(
180                 new ColorItem(R.color.grey300, ".grey300"),
181                 new ColorItem(R.color.grey600, ".grey600"));
182         result.put(
183                 new ColorItem(R.color.grey400, ".grey400"),
184                 new ColorItem(R.color.grey700, ".grey700"));
185         result.put(
186                 new ColorItem(R.color.grey200, ".grey200"),
187                 new ColorItem(R.color.grey800, ".grey800"));
188         result.put(
189                 new ColorItem(R.color.white, ".white"),
190                 new ColorItem(R.color.black, ".black"));
191         result.put(
192                 new ColorItem(R.color.orange600, ".orange600"),
193                 new ColorItem(R.color.orange300, ".orange300"));
194         result.put(
195                 new ColorItem(R.color.pink600, ".pink600"),
196                 new ColorItem(R.color.pink300, ".pink300"));
197         result.put(
198                 new ColorItem(R.color.purple600, ".purple600"),
199                 new ColorItem(R.color.purple300, ".purple300"));
200         result.put(
201                 new ColorItem(R.color.cyan600, ".cyan600"),
202                 new ColorItem(R.color.cyan300, ".cyan300"));
203         result.put(
204                 new ColorItem(R.color.orange400, ".orange400"),
205                 new ColorItem(R.color.orange100, ".orange100"));
206         result.put(
207                 new ColorItem(R.color.pink400, ".pink400"),
208                 new ColorItem(R.color.pink100, ".pink100"));
209         result.put(
210                 new ColorItem(R.color.cyan400, ".cyan400"),
211                 new ColorItem(R.color.cyan100, ".cyan100"));
212         return result;
213     }
214 
buildLightToDarkColorsMap()215     private static Map<ColorItem, ColorItem> buildLightToDarkColorsMap() {
216         Map<ColorItem, ColorItem> result = new HashMap<>();
217         result.put(
218                 new ColorItem(R.color.blue50, ".blue50"),
219                 new ColorItem(R.color.grey900, ".grey900"));
220         result.put(
221                 new ColorItem(R.color.green50, ".green50"),
222                 new ColorItem(R.color.grey900, ".grey900"));
223         result.put(
224                 new ColorItem(R.color.red50, ".red50"),
225                 new ColorItem(R.color.grey900, ".grey900"));
226         result.put(
227                 new ColorItem(R.color.yellow50, ".yellow50"),
228                 new ColorItem(R.color.grey900, ".grey900"));
229         result.put(
230                 new ColorItem(R.color.purple400, ".purple400"),
231                 new ColorItem(R.color.purple800, ".purple800"));
232         result.put(
233                 new ColorItem(R.color.purple300, ".purple300"),
234                 new ColorItem(R.color.purple800, ".purple800"));
235         return result;
236     }
237 }
238