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