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