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