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