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 android.stats.style.StyleEnums.COLOR_SOURCE_HOME_SCREEN_WALLPAPER;
19 import static android.stats.style.StyleEnums.COLOR_SOURCE_LOCK_SCREEN_WALLPAPER;
20 import static android.stats.style.StyleEnums.COLOR_SOURCE_PRESET_COLOR;
21 import static android.stats.style.StyleEnums.COLOR_SOURCE_UNSPECIFIED;
22 
23 import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_COLOR;
24 import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_SYSTEM_PALETTE;
25 import static com.android.customization.model.color.ColorOptionsProvider.COLOR_SOURCE_PRESET;
26 import static com.android.customization.model.color.ColorOptionsProvider.OVERLAY_COLOR_BOTH;
27 import static com.android.customization.model.color.ColorOptionsProvider.OVERLAY_COLOR_INDEX;
28 import static com.android.customization.model.color.ColorOptionsProvider.OVERLAY_COLOR_SOURCE;
29 import static com.android.customization.model.color.ColorOptionsProvider.OVERLAY_THEME_STYLE;
30 
31 import android.app.WallpaperColors;
32 import android.content.ContentResolver;
33 import android.content.Context;
34 import android.database.ContentObserver;
35 import android.graphics.Color;
36 import android.net.Uri;
37 import android.os.Handler;
38 import android.os.Looper;
39 import android.provider.Settings;
40 import android.text.TextUtils;
41 import android.util.Log;
42 
43 import androidx.annotation.Nullable;
44 import androidx.annotation.VisibleForTesting;
45 
46 import com.android.customization.model.CustomizationManager;
47 import com.android.customization.model.ResourceConstants;
48 import com.android.customization.model.color.ColorOptionsProvider.ColorSource;
49 import com.android.customization.model.theme.OverlayManagerCompat;
50 import com.android.customization.module.logging.ThemesUserEventLogger;
51 import com.android.themepicker.R;
52 
53 import org.json.JSONArray;
54 import org.json.JSONException;
55 import org.json.JSONObject;
56 
57 import java.util.HashMap;
58 import java.util.HashSet;
59 import java.util.Iterator;
60 import java.util.Map;
61 import java.util.Set;
62 import java.util.concurrent.ExecutorService;
63 import java.util.concurrent.Executors;
64 
65 /** The Color manager to manage Color bundle related operations. */
66 public class ColorCustomizationManager implements CustomizationManager<ColorOption> {
67 
68     private static final String TAG = "ColorCustomizationManager";
69 
70     private static final Set<String> COLOR_OVERLAY_SETTINGS = new HashSet<>();
71     static {
72         COLOR_OVERLAY_SETTINGS.add(OVERLAY_CATEGORY_SYSTEM_PALETTE);
73         COLOR_OVERLAY_SETTINGS.add(OVERLAY_CATEGORY_COLOR);
74         COLOR_OVERLAY_SETTINGS.add(OVERLAY_COLOR_SOURCE);
75         COLOR_OVERLAY_SETTINGS.add(OVERLAY_THEME_STYLE);
76     }
77 
78     private static ColorCustomizationManager sColorCustomizationManager;
79 
80     private final ColorOptionsProvider mProvider;
81     private final OverlayManagerCompat mOverlayManagerCompat;
82     private final ExecutorService mExecutorService;
83     private final ContentResolver mContentResolver;
84 
85     private Map<String, String> mCurrentOverlays;
86     @ColorSource private String mCurrentSource;
87     private String mCurrentStyle;
88     private WallpaperColors mHomeWallpaperColors;
89     private WallpaperColors mLockWallpaperColors;
90 
91     /** Returns the {@link ColorCustomizationManager} instance. */
getInstance(Context context, OverlayManagerCompat overlayManagerCompat)92     public static ColorCustomizationManager getInstance(Context context,
93             OverlayManagerCompat overlayManagerCompat) {
94         return getInstance(context, overlayManagerCompat, Executors.newSingleThreadExecutor());
95     }
96 
97     /** Returns the {@link ColorCustomizationManager} instance. */
98     @VisibleForTesting
getInstance(Context context, OverlayManagerCompat overlayManagerCompat, ExecutorService executorService)99     static ColorCustomizationManager getInstance(Context context,
100             OverlayManagerCompat overlayManagerCompat, ExecutorService executorService) {
101         if (sColorCustomizationManager == null) {
102             Context appContext = context.getApplicationContext();
103             sColorCustomizationManager = new ColorCustomizationManager(
104                     new ColorProvider(appContext,
105                             appContext.getString(R.string.themes_stub_package)),
106                     appContext.getContentResolver(), overlayManagerCompat,
107                     executorService);
108         }
109         return sColorCustomizationManager;
110     }
111 
112     @VisibleForTesting
ColorCustomizationManager(ColorOptionsProvider provider, ContentResolver contentResolver, OverlayManagerCompat overlayManagerCompat, ExecutorService executorService)113     ColorCustomizationManager(ColorOptionsProvider provider, ContentResolver contentResolver,
114             OverlayManagerCompat overlayManagerCompat, ExecutorService executorService) {
115         mProvider = provider;
116         mContentResolver = contentResolver;
117         mExecutorService = executorService;
118         ContentObserver observer = new ContentObserver(/* handler= */ null) {
119             @Override
120             public void onChange(boolean selfChange, Uri uri) {
121                 super.onChange(selfChange, uri);
122                 // Resets current overlays when system's theme setting is changed.
123                 if (TextUtils.equals(uri.getLastPathSegment(), ResourceConstants.THEME_SETTING)) {
124                     Log.i(TAG, "Resetting " + mCurrentOverlays + ", " + mCurrentStyle + ", "
125                             + mCurrentSource + " to null");
126                     mCurrentOverlays = null;
127                     mCurrentStyle = null;
128                     mCurrentSource = null;
129                 }
130             }
131         };
132         mContentResolver.registerContentObserver(
133                 Settings.Secure.CONTENT_URI, /* notifyForDescendants= */ true, observer);
134         mOverlayManagerCompat = overlayManagerCompat;
135     }
136 
137     @Override
isAvailable()138     public boolean isAvailable() {
139         return mOverlayManagerCompat.isAvailable() && mProvider.isAvailable();
140     }
141 
142     @Override
apply(ColorOption theme, Callback callback)143     public void apply(ColorOption theme, Callback callback) {
144         applyOverlays(theme, callback);
145     }
146 
applyOverlays(ColorOption colorOption, Callback callback)147     private void applyOverlays(ColorOption colorOption, Callback callback) {
148         mExecutorService.submit(() -> {
149             String currentStoredOverlays = getStoredOverlays();
150             if (TextUtils.isEmpty(currentStoredOverlays)) {
151                 currentStoredOverlays = "{}";
152             }
153             JSONObject overlaysJson = null;
154             try {
155                 overlaysJson = new JSONObject(currentStoredOverlays);
156                 JSONObject colorJson = colorOption.getJsonPackages(true);
157                 for (String setting : COLOR_OVERLAY_SETTINGS) {
158                     overlaysJson.remove(setting);
159                 }
160                 for (Iterator<String> it = colorJson.keys(); it.hasNext(); ) {
161                     String key = it.next();
162                     overlaysJson.put(key, colorJson.get(key));
163                 }
164                 overlaysJson.put(OVERLAY_COLOR_SOURCE, colorOption.getSource());
165                 overlaysJson.put(OVERLAY_COLOR_INDEX, String.valueOf(colorOption.getIndex()));
166                 overlaysJson.put(OVERLAY_THEME_STYLE,
167                         String.valueOf(colorOption.getStyle().toString()));
168 
169                 // OVERLAY_COLOR_BOTH is only for wallpaper color case, not preset.
170                 if (!COLOR_SOURCE_PRESET.equals(colorOption.getSource())) {
171                     boolean isForBoth =
172                             (mLockWallpaperColors == null || mLockWallpaperColors.equals(
173                                     mHomeWallpaperColors));
174                     overlaysJson.put(OVERLAY_COLOR_BOTH, isForBoth ? "1" : "0");
175                 } else {
176                     overlaysJson.remove(OVERLAY_COLOR_BOTH);
177                 }
178             } catch (JSONException e) {
179                 e.printStackTrace();
180             }
181             boolean allApplied = overlaysJson != null && Settings.Secure.putString(
182                     mContentResolver, ResourceConstants.THEME_SETTING, overlaysJson.toString());
183             new Handler(Looper.getMainLooper()).post(() -> {
184                 if (allApplied) {
185                     callback.onSuccess();
186                 } else {
187                     callback.onError(null);
188                 }
189             });
190         });
191     }
192 
193     @Override
fetchOptions(OptionsFetchedListener<ColorOption> callback, boolean reload)194     public void fetchOptions(OptionsFetchedListener<ColorOption> callback, boolean reload) {
195         WallpaperColors lockWallpaperColors = mLockWallpaperColors;
196         if (lockWallpaperColors != null && mLockWallpaperColors.equals(mHomeWallpaperColors)) {
197             lockWallpaperColors = null;
198         }
199         mProvider.fetch(callback, reload, mHomeWallpaperColors,
200                 lockWallpaperColors);
201     }
202 
203     /**
204      * Sets the current wallpaper colors to extract seeds from
205      */
setWallpaperColors(WallpaperColors homeColors, @Nullable WallpaperColors lockColors)206     public void setWallpaperColors(WallpaperColors homeColors,
207             @Nullable WallpaperColors lockColors) {
208         mHomeWallpaperColors = homeColors;
209         mLockWallpaperColors = lockColors;
210     }
211 
212     /**
213      * Gets current overlays mapping
214      * @return the {@link Map} of overlays
215      */
getCurrentOverlays()216     public Map<String, String> getCurrentOverlays() {
217         if (mCurrentOverlays == null) {
218             parseSettings(getStoredOverlays());
219         }
220         return mCurrentOverlays;
221     }
222 
223     /** */
getCurrentColorSourceForLogging()224     public int getCurrentColorSourceForLogging() {
225         String colorSource = getCurrentColorSource();
226         if (colorSource == null) {
227             return COLOR_SOURCE_UNSPECIFIED;
228         }
229         return switch (colorSource) {
230             case ColorOptionsProvider.COLOR_SOURCE_PRESET -> COLOR_SOURCE_PRESET_COLOR;
231             case ColorOptionsProvider.COLOR_SOURCE_HOME -> COLOR_SOURCE_HOME_SCREEN_WALLPAPER;
232             case ColorOptionsProvider.COLOR_SOURCE_LOCK -> COLOR_SOURCE_LOCK_SCREEN_WALLPAPER;
233             default -> COLOR_SOURCE_UNSPECIFIED;
234         };
235     }
236 
237     /** */
getCurrentStyleForLogging()238     public int getCurrentStyleForLogging() {
239         String style = getCurrentStyle();
240         return style != null ? style.hashCode() : 0;
241     }
242 
243     /** */
getCurrentSeedColorForLogging()244     public int getCurrentSeedColorForLogging() {
245         String seedColor = getCurrentOverlays().get(OVERLAY_CATEGORY_SYSTEM_PALETTE);
246         if (seedColor == null || seedColor.isEmpty()) {
247             return ThemesUserEventLogger.NULL_SEED_COLOR;
248         }
249         if (!seedColor.startsWith("#")) {
250             seedColor = "#" + seedColor;
251         }
252         return Color.parseColor(seedColor);
253     }
254 
255     /**
256      * @return The source of the currently applied color. One of
257      * {@link ColorOptionsProvider#COLOR_SOURCE_HOME},{@link ColorOptionsProvider#COLOR_SOURCE_LOCK}
258      * or {@link ColorOptionsProvider#COLOR_SOURCE_PRESET}.
259      */
260     @ColorSource
getCurrentColorSource()261     public @Nullable String getCurrentColorSource() {
262         if (mCurrentSource == null) {
263             parseSettings(getStoredOverlays());
264         }
265         return mCurrentSource;
266     }
267 
268     /**
269      * @return The style of the currently applied color. One of enum values in
270      * {@link com.android.systemui.monet.Style}.
271      */
getCurrentStyle()272     public @Nullable String getCurrentStyle() {
273         if (mCurrentStyle == null) {
274             parseSettings(getStoredOverlays());
275         }
276         return mCurrentStyle;
277     }
278 
getStoredOverlays()279     public String getStoredOverlays() {
280         return Settings.Secure.getString(mContentResolver, ResourceConstants.THEME_SETTING);
281     }
282 
283     @VisibleForTesting
parseSettings(String serializedJson)284     void parseSettings(String serializedJson) {
285         Map<String, String> allSettings = parseColorSettings(serializedJson);
286         mCurrentSource = allSettings.remove(OVERLAY_COLOR_SOURCE);
287         mCurrentStyle = allSettings.remove(OVERLAY_THEME_STYLE);
288         mCurrentOverlays = allSettings;
289     }
290 
parseColorSettings(String serializedJsonSettings)291     private Map<String, String> parseColorSettings(String serializedJsonSettings) {
292         Map<String, String> overlayPackages = new HashMap<>();
293         if (serializedJsonSettings != null) {
294             try {
295                 final JSONObject jsonPackages = new JSONObject(serializedJsonSettings);
296 
297                 JSONArray names = jsonPackages.names();
298                 if (names != null) {
299                     for (int i = 0; i < names.length(); i++) {
300                         String category = names.getString(i);
301                         if (COLOR_OVERLAY_SETTINGS.contains(category)) {
302                             try {
303                                 overlayPackages.put(category, jsonPackages.getString(category));
304                             } catch (JSONException e) {
305                                 Log.e(TAG, "parseColorOverlays: " + e.getLocalizedMessage(), e);
306                             }
307                         }
308                     }
309                 }
310             } catch (JSONException e) {
311                 Log.e(TAG, "parseColorOverlays: " + e.getLocalizedMessage(), e);
312             }
313         }
314         return overlayPackages;
315     }
316 }
317