1 /* 2 * Copyright (C) 2019 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.theme; 17 18 import static com.android.customization.model.ResourceConstants.ANDROID_PACKAGE; 19 import static com.android.customization.model.ResourceConstants.ICONS_FOR_PREVIEW; 20 import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_COLOR; 21 import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_FONT; 22 import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_ICON_ANDROID; 23 import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_ICON_LAUNCHER; 24 import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_ICON_SETTINGS; 25 import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_ICON_SYSUI; 26 import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_ICON_THEMEPICKER; 27 import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_SHAPE; 28 import static com.android.customization.model.ResourceConstants.SYSUI_PACKAGE; 29 30 import android.content.Context; 31 import android.content.pm.ApplicationInfo; 32 import android.content.pm.PackageManager.NameNotFoundException; 33 import android.content.res.Resources.NotFoundException; 34 import android.graphics.drawable.Drawable; 35 import android.text.TextUtils; 36 import android.util.Log; 37 38 import androidx.annotation.Nullable; 39 40 import com.android.customization.model.CustomizationManager.OptionsFetchedListener; 41 import com.android.customization.model.ResourcesApkProvider; 42 import com.android.customization.model.theme.ThemeBundle.Builder; 43 import com.android.customization.model.theme.ThemeBundle.PreviewInfo.ShapeAppIcon; 44 import com.android.customization.model.theme.custom.CustomTheme; 45 import com.android.customization.module.CustomizationPreferences; 46 import com.android.wallpaper.R; 47 48 import org.json.JSONArray; 49 import org.json.JSONException; 50 import org.json.JSONObject; 51 52 import java.util.ArrayList; 53 import java.util.HashMap; 54 import java.util.Iterator; 55 import java.util.List; 56 import java.util.Map; 57 58 /** 59 * Default implementation of {@link ThemeBundleProvider} that reads Themes' overlays from a stub APK. 60 */ 61 public class DefaultThemeProvider extends ResourcesApkProvider implements ThemeBundleProvider { 62 63 private static final String TAG = "DefaultThemeProvider"; 64 65 private static final String THEMES_ARRAY = "themes"; 66 private static final String TITLE_PREFIX = "theme_title_"; 67 private static final String FONT_PREFIX = "theme_overlay_font_"; 68 private static final String COLOR_PREFIX = "theme_overlay_color_"; 69 private static final String SHAPE_PREFIX = "theme_overlay_shape_"; 70 private static final String ICON_ANDROID_PREFIX = "theme_overlay_icon_android_"; 71 private static final String ICON_LAUNCHER_PREFIX = "theme_overlay_icon_launcher_"; 72 private static final String ICON_THEMEPICKER_PREFIX = "theme_overlay_icon_themepicker_"; 73 private static final String ICON_SETTINGS_PREFIX = "theme_overlay_icon_settings_"; 74 private static final String ICON_SYSUI_PREFIX = "theme_overlay_icon_sysui_"; 75 76 private static final String DEFAULT_THEME_NAME= "default"; 77 private static final String THEME_TITLE_FIELD = "_theme_title"; 78 private static final String THEME_ID_FIELD = "_theme_id"; 79 80 private final OverlayThemeExtractor mOverlayProvider; 81 private List<ThemeBundle> mThemes; 82 private final CustomizationPreferences mCustomizationPreferences; 83 DefaultThemeProvider(Context context, CustomizationPreferences customizationPrefs)84 public DefaultThemeProvider(Context context, CustomizationPreferences customizationPrefs) { 85 super(context, context.getString(R.string.themes_stub_package)); 86 mOverlayProvider = new OverlayThemeExtractor(context); 87 mCustomizationPreferences = customizationPrefs; 88 } 89 90 @Override fetch(OptionsFetchedListener<ThemeBundle> callback, boolean reload)91 public void fetch(OptionsFetchedListener<ThemeBundle> callback, boolean reload) { 92 if (mThemes == null || reload) { 93 mThemes = new ArrayList<>(); 94 loadAll(); 95 } 96 97 if(callback != null) { 98 callback.onOptionsLoaded(mThemes); 99 } 100 } 101 102 @Override isAvailable()103 public boolean isAvailable() { 104 return mOverlayProvider.isAvailable() && super.isAvailable(); 105 } 106 loadAll()107 private void loadAll() { 108 // Add "Custom" option at the beginning. 109 mThemes.add(new CustomTheme.Builder() 110 .setId(CustomTheme.newId()) 111 .setTitle(mContext.getString(R.string.custom_theme)) 112 .build(mContext)); 113 114 addDefaultTheme(); 115 116 String[] themeNames = getItemsFromStub(THEMES_ARRAY); 117 118 for (String themeName : themeNames) { 119 // Default theme needs special treatment (see #addDefaultTheme()) 120 if (DEFAULT_THEME_NAME.equals(themeName)) { 121 continue; 122 } 123 ThemeBundle.Builder builder = new Builder(); 124 try { 125 builder.setTitle(mStubApkResources.getString( 126 mStubApkResources.getIdentifier(TITLE_PREFIX + themeName, 127 "string", mStubPackageName))); 128 129 String shapeOverlayPackage = getOverlayPackage(SHAPE_PREFIX, themeName); 130 mOverlayProvider.addShapeOverlay(builder, shapeOverlayPackage); 131 132 String fontOverlayPackage = getOverlayPackage(FONT_PREFIX, themeName); 133 mOverlayProvider.addFontOverlay(builder, fontOverlayPackage); 134 135 String colorOverlayPackage = getOverlayPackage(COLOR_PREFIX, themeName); 136 mOverlayProvider.addColorOverlay(builder, colorOverlayPackage); 137 138 String iconAndroidOverlayPackage = getOverlayPackage(ICON_ANDROID_PREFIX, 139 themeName); 140 141 mOverlayProvider.addAndroidIconOverlay(builder, iconAndroidOverlayPackage); 142 143 String iconSysUiOverlayPackage = getOverlayPackage(ICON_SYSUI_PREFIX, themeName); 144 145 mOverlayProvider.addSysUiIconOverlay(builder, iconSysUiOverlayPackage); 146 147 String iconLauncherOverlayPackage = getOverlayPackage(ICON_LAUNCHER_PREFIX, 148 themeName); 149 mOverlayProvider.addNoPreviewIconOverlay(builder, iconLauncherOverlayPackage); 150 151 String iconThemePickerOverlayPackage = getOverlayPackage(ICON_THEMEPICKER_PREFIX, 152 themeName); 153 mOverlayProvider.addNoPreviewIconOverlay(builder, 154 iconThemePickerOverlayPackage); 155 156 String iconSettingsOverlayPackage = getOverlayPackage(ICON_SETTINGS_PREFIX, 157 themeName); 158 159 mOverlayProvider.addNoPreviewIconOverlay(builder, iconSettingsOverlayPackage); 160 161 mThemes.add(builder.build(mContext)); 162 } catch (NameNotFoundException | NotFoundException e) { 163 Log.w(TAG, String.format("Couldn't load part of theme %s, will skip it", themeName), 164 e); 165 } 166 } 167 168 addCustomThemes(); 169 } 170 171 /** 172 * Default theme requires different treatment: if there are overlay packages specified in the 173 * stub apk, we'll use those, otherwise we'll get the System default values. But we cannot skip 174 * the default theme. 175 */ addDefaultTheme()176 private void addDefaultTheme() { 177 ThemeBundle.Builder builder = new Builder().asDefault(); 178 179 int titleId = mStubApkResources.getIdentifier(TITLE_PREFIX + DEFAULT_THEME_NAME, 180 "string", mStubPackageName); 181 if (titleId > 0) { 182 builder.setTitle(mStubApkResources.getString(titleId)); 183 } else { 184 builder.setTitle(mContext.getString(R.string.default_theme_title)); 185 } 186 187 try { 188 String colorOverlayPackage = getOverlayPackage(COLOR_PREFIX, DEFAULT_THEME_NAME); 189 mOverlayProvider.addColorOverlay(builder, colorOverlayPackage); 190 } catch (NameNotFoundException | NotFoundException e) { 191 Log.d(TAG, "Didn't find color overlay for default theme, will use system default"); 192 mOverlayProvider.addSystemDefaultColor(builder); 193 } 194 195 try { 196 String fontOverlayPackage = getOverlayPackage(FONT_PREFIX, DEFAULT_THEME_NAME); 197 mOverlayProvider.addFontOverlay(builder, fontOverlayPackage); 198 } catch (NameNotFoundException | NotFoundException e) { 199 Log.d(TAG, "Didn't find font overlay for default theme, will use system default"); 200 mOverlayProvider.addSystemDefaultFont(builder); 201 } 202 203 try { 204 String shapeOverlayPackage = getOverlayPackage(SHAPE_PREFIX, DEFAULT_THEME_NAME); 205 mOverlayProvider.addShapeOverlay(builder ,shapeOverlayPackage, false); 206 } catch (NameNotFoundException | NotFoundException e) { 207 Log.d(TAG, "Didn't find shape overlay for default theme, will use system default"); 208 mOverlayProvider.addSystemDefaultShape(builder); 209 } 210 211 List<ShapeAppIcon> icons = new ArrayList<>(); 212 for (String packageName : mOverlayProvider.getShapePreviewIconPackages()) { 213 Drawable icon = null; 214 CharSequence name = null; 215 try { 216 icon = mContext.getPackageManager().getApplicationIcon(packageName); 217 ApplicationInfo appInfo = mContext.getPackageManager() 218 .getApplicationInfo(packageName, /* flag= */ 0); 219 name = mContext.getPackageManager().getApplicationLabel(appInfo); 220 } catch (NameNotFoundException e) { 221 Log.d(TAG, "Couldn't find app " + packageName + ", won't use it for icon shape" 222 + "preview"); 223 } finally { 224 if (icon != null && !TextUtils.isEmpty(name)) { 225 icons.add(new ShapeAppIcon(icon, name)); 226 } 227 } 228 } 229 builder.setShapePreviewIcons(icons); 230 231 try { 232 String iconAndroidOverlayPackage = getOverlayPackage(ICON_ANDROID_PREFIX, 233 DEFAULT_THEME_NAME); 234 mOverlayProvider.addAndroidIconOverlay(builder, iconAndroidOverlayPackage); 235 } catch (NameNotFoundException | NotFoundException e) { 236 Log.d(TAG, "Didn't find Android icons overlay for default theme, using system default"); 237 mOverlayProvider.addSystemDefaultIcons(builder, ANDROID_PACKAGE, ICONS_FOR_PREVIEW); 238 } 239 240 try { 241 String iconSysUiOverlayPackage = getOverlayPackage(ICON_SYSUI_PREFIX, 242 DEFAULT_THEME_NAME); 243 mOverlayProvider.addSysUiIconOverlay(builder, iconSysUiOverlayPackage); 244 } catch (NameNotFoundException | NotFoundException e) { 245 Log.d(TAG, 246 "Didn't find SystemUi icons overlay for default theme, using system default"); 247 mOverlayProvider.addSystemDefaultIcons(builder, SYSUI_PACKAGE, ICONS_FOR_PREVIEW); 248 } 249 250 mThemes.add(builder.build(mContext)); 251 } 252 253 @Override storeCustomTheme(CustomTheme theme)254 public void storeCustomTheme(CustomTheme theme) { 255 if (mThemes == null) { 256 fetch(options -> { 257 addCustomThemeAndStore(theme); 258 }, false); 259 } else { 260 addCustomThemeAndStore(theme); 261 } 262 } 263 addCustomThemeAndStore(CustomTheme theme)264 private void addCustomThemeAndStore(CustomTheme theme) { 265 if (!mThemes.contains(theme)) { 266 mThemes.add(theme); 267 } else { 268 mThemes.replaceAll(t -> theme.equals(t) ? theme : t); 269 } 270 JSONArray themesArray = new JSONArray(); 271 mThemes.stream() 272 .filter(themeBundle -> themeBundle instanceof CustomTheme 273 && !themeBundle.getPackagesByCategory().isEmpty()) 274 .forEachOrdered(themeBundle -> addThemeBundleToArray(themesArray, themeBundle)); 275 mCustomizationPreferences.storeCustomThemes(themesArray.toString()); 276 } 277 addThemeBundleToArray(JSONArray themesArray, ThemeBundle themeBundle)278 private void addThemeBundleToArray(JSONArray themesArray, ThemeBundle themeBundle) { 279 JSONObject jsonPackages = themeBundle.getJsonPackages(false); 280 try { 281 jsonPackages.put(THEME_TITLE_FIELD, themeBundle.getTitle()); 282 if (themeBundle instanceof CustomTheme) { 283 jsonPackages.put(THEME_ID_FIELD, ((CustomTheme)themeBundle).getId()); 284 } 285 } catch (JSONException e) { 286 Log.w("Exception saving theme's title", e); 287 } 288 themesArray.put(jsonPackages); 289 } 290 291 @Override removeCustomTheme(CustomTheme theme)292 public void removeCustomTheme(CustomTheme theme) { 293 JSONArray themesArray = new JSONArray(); 294 mThemes.stream() 295 .filter(themeBundle -> themeBundle instanceof CustomTheme 296 && ((CustomTheme) themeBundle).isDefined()) 297 .forEachOrdered(customTheme -> { 298 if (!customTheme.equals(theme)) { 299 addThemeBundleToArray(themesArray, customTheme); 300 } 301 }); 302 mCustomizationPreferences.storeCustomThemes(themesArray.toString()); 303 } 304 addCustomThemes()305 private void addCustomThemes() { 306 String serializedThemes = mCustomizationPreferences.getSerializedCustomThemes(); 307 int customThemesCount = 0; 308 if (!TextUtils.isEmpty(serializedThemes)) { 309 try { 310 JSONArray customThemes = new JSONArray(serializedThemes); 311 for (int i = 0; i < customThemes.length(); i++) { 312 JSONObject jsonTheme = customThemes.getJSONObject(i); 313 CustomTheme.Builder builder = new CustomTheme.Builder(); 314 try { 315 convertJsonToBuilder(jsonTheme, builder); 316 } catch (NameNotFoundException | NotFoundException e) { 317 Log.i(TAG, "Couldn't parse serialized custom theme", e); 318 builder = null; 319 } 320 if (builder != null) { 321 if (TextUtils.isEmpty(builder.getTitle())) { 322 builder.setTitle(mContext.getString(R.string.custom_theme_title, 323 customThemesCount + 1)); 324 } 325 mThemes.add(builder.build(mContext)); 326 } else { 327 Log.w(TAG, "Couldn't read stored custom theme, resetting"); 328 mThemes.add(new CustomTheme.Builder() 329 .setId(CustomTheme.newId()) 330 .setTitle(mContext.getString( 331 R.string.custom_theme_title, customThemesCount + 1)) 332 .build(mContext)); 333 } 334 customThemesCount++; 335 } 336 } catch (JSONException e) { 337 Log.w(TAG, "Couldn't read stored custom theme, resetting", e); 338 mThemes.add(new CustomTheme.Builder() 339 .setId(CustomTheme.newId()) 340 .setTitle(mContext.getString( 341 R.string.custom_theme_title, customThemesCount + 1)) 342 .build(mContext)); 343 } 344 } 345 } 346 347 @Nullable 348 @Override parseThemeBundle(String serializedTheme)349 public ThemeBundle.Builder parseThemeBundle(String serializedTheme) throws JSONException { 350 JSONObject theme = new JSONObject(serializedTheme); 351 try { 352 ThemeBundle.Builder builder = new ThemeBundle.Builder(); 353 convertJsonToBuilder(theme, builder); 354 return builder; 355 } catch (NameNotFoundException | NotFoundException e) { 356 Log.i(TAG, "Couldn't parse serialized custom theme", e); 357 return null; 358 } 359 } 360 361 @Nullable 362 @Override parseCustomTheme(String serializedTheme)363 public CustomTheme.Builder parseCustomTheme(String serializedTheme) throws JSONException { 364 JSONObject theme = new JSONObject(serializedTheme); 365 try { 366 CustomTheme.Builder builder = new CustomTheme.Builder(); 367 convertJsonToBuilder(theme, builder); 368 return builder; 369 } catch (NameNotFoundException | NotFoundException e) { 370 Log.i(TAG, "Couldn't parse serialized custom theme", e); 371 return null; 372 } 373 } 374 convertJsonToBuilder(JSONObject theme, ThemeBundle.Builder builder)375 private void convertJsonToBuilder(JSONObject theme, ThemeBundle.Builder builder) 376 throws JSONException, NameNotFoundException, NotFoundException { 377 Map<String, String> customPackages = new HashMap<>(); 378 Iterator<String> keysIterator = theme.keys(); 379 380 while (keysIterator.hasNext()) { 381 String category = keysIterator.next(); 382 customPackages.put(category, theme.getString(category)); 383 } 384 mOverlayProvider.addShapeOverlay(builder, 385 customPackages.get(OVERLAY_CATEGORY_SHAPE)); 386 mOverlayProvider.addFontOverlay(builder, 387 customPackages.get(OVERLAY_CATEGORY_FONT)); 388 mOverlayProvider.addColorOverlay(builder, 389 customPackages.get(OVERLAY_CATEGORY_COLOR)); 390 mOverlayProvider.addAndroidIconOverlay(builder, 391 customPackages.get(OVERLAY_CATEGORY_ICON_ANDROID)); 392 mOverlayProvider.addSysUiIconOverlay(builder, 393 customPackages.get(OVERLAY_CATEGORY_ICON_SYSUI)); 394 mOverlayProvider.addNoPreviewIconOverlay(builder, 395 customPackages.get(OVERLAY_CATEGORY_ICON_SETTINGS)); 396 mOverlayProvider.addNoPreviewIconOverlay(builder, 397 customPackages.get(OVERLAY_CATEGORY_ICON_LAUNCHER)); 398 mOverlayProvider.addNoPreviewIconOverlay(builder, 399 customPackages.get(OVERLAY_CATEGORY_ICON_THEMEPICKER)); 400 if (theme.has(THEME_TITLE_FIELD)) { 401 builder.setTitle(theme.getString(THEME_TITLE_FIELD)); 402 } 403 if (builder instanceof CustomTheme.Builder && theme.has(THEME_ID_FIELD)) { 404 ((CustomTheme.Builder) builder).setId(theme.getString(THEME_ID_FIELD)); 405 } 406 } 407 408 @Override findEquivalent(ThemeBundle other)409 public ThemeBundle findEquivalent(ThemeBundle other) { 410 if (mThemes == null) { 411 return null; 412 } 413 for (ThemeBundle theme : mThemes) { 414 if (theme.isEquivalent(other)) { 415 return theme; 416 } 417 } 418 return null; 419 } 420 getOverlayPackage(String prefix, String themeName)421 private String getOverlayPackage(String prefix, String themeName) { 422 return getItemStringFromStub(prefix, themeName); 423 } 424 } 425