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