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 package com.android.customization.model.color;
17 
18 import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_COLOR;
19 import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_SYSTEM_PALETTE;
20 
21 import android.content.Context;
22 import android.graphics.Color;
23 import android.text.TextUtils;
24 import android.util.Log;
25 
26 import androidx.annotation.VisibleForTesting;
27 
28 import com.android.customization.model.CustomizationManager;
29 import com.android.customization.model.CustomizationOption;
30 import com.android.customization.model.color.ColorOptionsProvider.ColorSource;
31 import com.android.customization.module.logging.ThemesUserEventLogger;
32 import com.android.systemui.monet.Style;
33 import com.android.themepicker.R;
34 
35 import org.json.JSONException;
36 import org.json.JSONObject;
37 
38 import java.util.Collections;
39 import java.util.HashSet;
40 import java.util.Iterator;
41 import java.util.Map;
42 import java.util.Set;
43 import java.util.stream.Collectors;
44 
45 /**
46  * Represents a color choice for the user.
47  * This could be a preset color or those obtained from a wallpaper.
48  */
49 public abstract class ColorOption implements CustomizationOption<ColorOption> {
50 
51     private static final String TAG = "ColorOption";
52     private static final String EMPTY_JSON = "{}";
53     @VisibleForTesting
54     static final String TIMESTAMP_FIELD = "_applied_timestamp";
55 
56     protected final Map<String, String> mPackagesByCategory;
57     private final String mTitle;
58     private final boolean mIsDefault;
59     private final Style mStyle;
60     private final int mIndex;
61     private CharSequence mContentDescription;
62 
ColorOption(String title, Map<String, String> overlayPackages, boolean isDefault, Style style, int index)63     protected ColorOption(String title, Map<String, String> overlayPackages, boolean isDefault,
64             Style style, int index) {
65         mTitle = title;
66         mIsDefault = isDefault;
67         mStyle = style;
68         mIndex = index;
69         mPackagesByCategory = Collections.unmodifiableMap(removeNullValues(overlayPackages));
70     }
71 
72     @Override
getTitle()73     public String getTitle() {
74         return mTitle;
75     }
76 
77     @Override
isActive(CustomizationManager<ColorOption> manager)78     public boolean isActive(CustomizationManager<ColorOption> manager) {
79         ColorCustomizationManager colorManager = (ColorCustomizationManager) manager;
80 
81         String currentStyle = colorManager.getCurrentStyle();
82         if (TextUtils.isEmpty(currentStyle)) {
83             currentStyle = Style.TONAL_SPOT.toString();
84         }
85         boolean isCurrentStyle = TextUtils.equals(getStyle().toString(), currentStyle);
86 
87         if (mIsDefault) {
88             String serializedOverlays = colorManager.getStoredOverlays();
89             // a default color option is active if the manager has no stored overlays or current
90             // overlays, or the stored overlay does not contain either category system palette or
91             // category color
92             return (TextUtils.isEmpty(serializedOverlays) || EMPTY_JSON.equals(serializedOverlays)
93                     || colorManager.getCurrentOverlays().isEmpty() || !(serializedOverlays.contains(
94                     OVERLAY_CATEGORY_SYSTEM_PALETTE) || serializedOverlays.contains(
95                     OVERLAY_CATEGORY_COLOR))) && isCurrentStyle;
96         } else {
97             Map<String, String> currentOverlays = colorManager.getCurrentOverlays();
98             String currentSource = colorManager.getCurrentColorSource();
99             boolean isCurrentSource = TextUtils.isEmpty(currentSource) || getSource().equals(
100                     currentSource);
101             return isCurrentSource && isCurrentStyle && mPackagesByCategory.equals(currentOverlays);
102         }
103     }
104 
105     /**
106      * Gets the seed color from the overlay packages for logging.
107      *
108      * @return an int representing the seed color, or NULL_SEED_COLOR
109      */
getSeedColorForLogging()110     public int getSeedColorForLogging() {
111         String seedColor = mPackagesByCategory.get(OVERLAY_CATEGORY_SYSTEM_PALETTE);
112         if (seedColor == null || seedColor.isEmpty()) {
113             return ThemesUserEventLogger.NULL_SEED_COLOR;
114         }
115         if (!seedColor.startsWith("#")) {
116             seedColor = "#" + seedColor;
117         }
118         return Color.parseColor(seedColor);
119     }
120 
121     /**
122      * This is similar to #equals() but it only compares this theme's packages with the other, that
123      * is, it will return true if applying this theme has the same effect of applying the given one.
124      */
isEquivalent(ColorOption other)125     public boolean isEquivalent(ColorOption other) {
126         if (other == null) {
127             return false;
128         }
129         if (mStyle != other.getStyle()) {
130             return false;
131         }
132         String thisSerializedPackages = getSerializedPackages();
133         if (mIsDefault || TextUtils.isEmpty(thisSerializedPackages)
134                 || EMPTY_JSON.equals(thisSerializedPackages)) {
135             String otherSerializedPackages = other.getSerializedPackages();
136             return other.isDefault() || TextUtils.isEmpty(otherSerializedPackages)
137                     || EMPTY_JSON.equals(otherSerializedPackages);
138         }
139         // Map#equals ensures keys and values are compared.
140         return mPackagesByCategory.equals(other.mPackagesByCategory);
141     }
142 
143     /**
144      * Returns the {@link PreviewInfo} object for this ColorOption
145      */
getPreviewInfo()146     public abstract PreviewInfo getPreviewInfo();
147 
isDefault()148     boolean isDefault() {
149         return mIsDefault;
150     }
151 
getPackagesByCategory()152     public Map<String, String> getPackagesByCategory() {
153         return mPackagesByCategory;
154     }
155 
getSerializedPackages()156     public String getSerializedPackages() {
157         return getJsonPackages(false).toString();
158     }
159 
getSerializedPackagesWithTimestamp()160     public String getSerializedPackagesWithTimestamp() {
161         return getJsonPackages(true).toString();
162     }
163 
164     /**
165      * Get a JSONObject representation of this color option, with the current values for each
166      * field, and optionally a {@link TIMESTAMP_FIELD} field.
167      * @param insertTimestamp whether to add a field with the current timestamp
168      * @return the JSONObject for this color option
169      */
getJsonPackages(boolean insertTimestamp)170     public JSONObject getJsonPackages(boolean insertTimestamp) {
171         JSONObject json;
172         if (isDefault()) {
173             json = new JSONObject();
174         } else {
175             json = new JSONObject(mPackagesByCategory);
176             // Remove items with null values to avoid deserialization issues.
177             removeNullValues(json);
178         }
179         if (insertTimestamp) {
180             try {
181                 json.put(TIMESTAMP_FIELD, System.currentTimeMillis());
182             } catch (JSONException e) {
183                 Log.e(TAG, "Couldn't add timestamp to serialized themebundle");
184             }
185         }
186         return json;
187     }
188 
removeNullValues(JSONObject json)189     private void removeNullValues(JSONObject json) {
190         Iterator<String> keys = json.keys();
191         Set<String> keysToRemove = new HashSet<>();
192         while (keys.hasNext()) {
193             String key = keys.next();
194             if (json.isNull(key)) {
195                 keysToRemove.add(key);
196             }
197         }
198         for (String key : keysToRemove) {
199             json.remove(key);
200         }
201     }
202 
removeNullValues(Map<String, String> map)203     private Map<String, String> removeNullValues(Map<String, String> map) {
204         return map.entrySet()
205                 .stream()
206                 .filter(entry -> entry.getValue() != null)
207                 .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
208     }
209 
210     /** */
getContentDescription(Context context)211     public CharSequence getContentDescription(Context context) {
212         if (mContentDescription == null) {
213             CharSequence defaultName = context.getString(R.string.default_theme_title);
214             if (isDefault()) {
215                 mContentDescription = defaultName;
216             } else {
217                 mContentDescription = mTitle;
218             }
219         }
220         return mContentDescription;
221     }
222 
223     /**
224      * @return the source of this color option
225      */
226     @ColorSource
getSource()227     public abstract String getSource();
228 
229     /**
230      * @return the source of this color option for logging
231      */
232     @ThemesUserEventLogger.ColorSource
getSourceForLogging()233     public abstract int getSourceForLogging();
234 
235     /**
236      * @return the style of this color option
237      */
getStyle()238     public Style getStyle() {
239         return mStyle;
240     }
241 
242     /**
243      * @return the style of this color option for logging
244      */
getStyleForLogging()245     public abstract int getStyleForLogging();
246 
247     /**
248      * @return the index of this color option
249      */
getIndex()250     public int getIndex() {
251         return mIndex;
252     }
253 
254     /**
255      * The preview information of {@link ColorOption}
256      */
257     public interface PreviewInfo {
258     }
259 
260 }
261