| /* |
| * Copyright (C) 2019 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.android.customization.model.theme; |
| |
| import static com.android.customization.model.ResourceConstants.ANDROID_PACKAGE; |
| import static com.android.customization.model.ResourceConstants.ICONS_FOR_PREVIEW; |
| import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_COLOR; |
| import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_FONT; |
| import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_ICON_ANDROID; |
| import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_ICON_LAUNCHER; |
| import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_ICON_SETTINGS; |
| import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_ICON_SYSUI; |
| import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_ICON_THEMEPICKER; |
| import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_SHAPE; |
| import static com.android.customization.model.ResourceConstants.SYSUI_PACKAGE; |
| |
| import android.content.Context; |
| import android.content.pm.ApplicationInfo; |
| import android.content.pm.PackageManager.NameNotFoundException; |
| import android.content.res.Resources.NotFoundException; |
| import android.graphics.drawable.Drawable; |
| import android.text.TextUtils; |
| import android.util.Log; |
| |
| import androidx.annotation.Nullable; |
| |
| import com.android.customization.model.CustomizationManager.OptionsFetchedListener; |
| import com.android.customization.model.ResourcesApkProvider; |
| import com.android.customization.model.theme.ThemeBundle.Builder; |
| import com.android.customization.model.theme.ThemeBundle.PreviewInfo.ShapeAppIcon; |
| import com.android.customization.model.theme.custom.CustomTheme; |
| import com.android.customization.module.CustomizationPreferences; |
| import com.android.wallpaper.R; |
| import com.android.wallpaper.asset.ResourceAsset; |
| |
| import com.bumptech.glide.request.RequestOptions; |
| import com.google.android.apps.wallpaper.asset.ThemeBundleThumbAsset; |
| |
| import org.json.JSONArray; |
| import org.json.JSONException; |
| import org.json.JSONObject; |
| |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| |
| /** |
| * Default implementation of {@link ThemeBundleProvider} that reads Themes' overlays from a stub APK. |
| */ |
| public class DefaultThemeProvider extends ResourcesApkProvider implements ThemeBundleProvider { |
| |
| private static final String TAG = "DefaultThemeProvider"; |
| |
| private static final String THEMES_ARRAY = "themes"; |
| private static final String TITLE_PREFIX = "theme_title_"; |
| private static final String FONT_PREFIX = "theme_overlay_font_"; |
| private static final String COLOR_PREFIX = "theme_overlay_color_"; |
| private static final String SHAPE_PREFIX = "theme_overlay_shape_"; |
| private static final String ICON_ANDROID_PREFIX = "theme_overlay_icon_android_"; |
| private static final String ICON_LAUNCHER_PREFIX = "theme_overlay_icon_launcher_"; |
| private static final String ICON_THEMEPICKER_PREFIX = "theme_overlay_icon_themepicker_"; |
| private static final String ICON_SETTINGS_PREFIX = "theme_overlay_icon_settings_"; |
| private static final String ICON_SYSUI_PREFIX = "theme_overlay_icon_sysui_"; |
| |
| private static final String DEFAULT_THEME_NAME= "default"; |
| private static final String THEME_TITLE_FIELD = "_theme_title"; |
| private static final String THEME_ID_FIELD = "_theme_id"; |
| |
| private final OverlayThemeExtractor mOverlayProvider; |
| private List<ThemeBundle> mThemes; |
| private final CustomizationPreferences mCustomizationPreferences; |
| |
| public DefaultThemeProvider(Context context, CustomizationPreferences customizationPrefs) { |
| super(context, context.getString(R.string.themes_stub_package)); |
| mOverlayProvider = new OverlayThemeExtractor(context); |
| mCustomizationPreferences = customizationPrefs; |
| } |
| |
| @Override |
| public void fetch(OptionsFetchedListener<ThemeBundle> callback, boolean reload) { |
| if (mThemes == null || reload) { |
| mThemes = new ArrayList<>(); |
| loadAll(); |
| } |
| |
| if(callback != null) { |
| callback.onOptionsLoaded(mThemes); |
| } |
| } |
| |
| @Override |
| public boolean isAvailable() { |
| return mOverlayProvider.isAvailable() && super.isAvailable(); |
| } |
| |
| private void loadAll() { |
| // Add "Custom" option at the beginning. |
| mThemes.add(new CustomTheme.Builder() |
| .setId(CustomTheme.newId()) |
| .setTitle(mContext.getString(R.string.custom_theme)) |
| .build(mContext)); |
| |
| addDefaultTheme(); |
| |
| String[] themeNames = getItemsFromStub(THEMES_ARRAY); |
| |
| for (String themeName : themeNames) { |
| // Default theme needs special treatment (see #addDefaultTheme()) |
| if (DEFAULT_THEME_NAME.equals(themeName)) { |
| continue; |
| } |
| ThemeBundle.Builder builder = new Builder(); |
| try { |
| builder.setTitle(mStubApkResources.getString( |
| mStubApkResources.getIdentifier(TITLE_PREFIX + themeName, |
| "string", mStubPackageName))); |
| |
| String shapeOverlayPackage = getOverlayPackage(SHAPE_PREFIX, themeName); |
| mOverlayProvider.addShapeOverlay(builder, shapeOverlayPackage); |
| |
| String fontOverlayPackage = getOverlayPackage(FONT_PREFIX, themeName); |
| mOverlayProvider.addFontOverlay(builder, fontOverlayPackage); |
| |
| String colorOverlayPackage = getOverlayPackage(COLOR_PREFIX, themeName); |
| mOverlayProvider.addColorOverlay(builder, colorOverlayPackage); |
| |
| String iconAndroidOverlayPackage = getOverlayPackage(ICON_ANDROID_PREFIX, |
| themeName); |
| |
| mOverlayProvider.addAndroidIconOverlay(builder, iconAndroidOverlayPackage); |
| |
| String iconSysUiOverlayPackage = getOverlayPackage(ICON_SYSUI_PREFIX, themeName); |
| |
| mOverlayProvider.addSysUiIconOverlay(builder, iconSysUiOverlayPackage); |
| |
| String iconLauncherOverlayPackage = getOverlayPackage(ICON_LAUNCHER_PREFIX, |
| themeName); |
| mOverlayProvider.addNoPreviewIconOverlay(builder, iconLauncherOverlayPackage); |
| |
| String iconThemePickerOverlayPackage = getOverlayPackage(ICON_THEMEPICKER_PREFIX, |
| themeName); |
| mOverlayProvider.addNoPreviewIconOverlay(builder, |
| iconThemePickerOverlayPackage); |
| |
| String iconSettingsOverlayPackage = getOverlayPackage(ICON_SETTINGS_PREFIX, |
| themeName); |
| |
| mOverlayProvider.addNoPreviewIconOverlay(builder, iconSettingsOverlayPackage); |
| |
| mThemes.add(builder.build(mContext)); |
| } catch (NameNotFoundException | NotFoundException e) { |
| Log.w(TAG, String.format("Couldn't load part of theme %s, will skip it", themeName), |
| e); |
| } |
| } |
| |
| addCustomThemes(); |
| } |
| |
| /** |
| * Default theme requires different treatment: if there are overlay packages specified in the |
| * stub apk, we'll use those, otherwise we'll get the System default values. But we cannot skip |
| * the default theme. |
| */ |
| private void addDefaultTheme() { |
| ThemeBundle.Builder builder = new Builder().asDefault(); |
| |
| int titleId = mStubApkResources.getIdentifier(TITLE_PREFIX + DEFAULT_THEME_NAME, |
| "string", mStubPackageName); |
| if (titleId > 0) { |
| builder.setTitle(mStubApkResources.getString(titleId)); |
| } else { |
| builder.setTitle(mContext.getString(R.string.default_theme_title)); |
| } |
| |
| try { |
| String colorOverlayPackage = getOverlayPackage(COLOR_PREFIX, DEFAULT_THEME_NAME); |
| mOverlayProvider.addColorOverlay(builder, colorOverlayPackage); |
| } catch (NameNotFoundException | NotFoundException e) { |
| Log.d(TAG, "Didn't find color overlay for default theme, will use system default"); |
| mOverlayProvider.addSystemDefaultColor(builder); |
| } |
| |
| try { |
| String fontOverlayPackage = getOverlayPackage(FONT_PREFIX, DEFAULT_THEME_NAME); |
| mOverlayProvider.addFontOverlay(builder, fontOverlayPackage); |
| } catch (NameNotFoundException | NotFoundException e) { |
| Log.d(TAG, "Didn't find font overlay for default theme, will use system default"); |
| mOverlayProvider.addSystemDefaultFont(builder); |
| } |
| |
| try { |
| String shapeOverlayPackage = getOverlayPackage(SHAPE_PREFIX, DEFAULT_THEME_NAME); |
| mOverlayProvider.addShapeOverlay(builder ,shapeOverlayPackage, false); |
| } catch (NameNotFoundException | NotFoundException e) { |
| Log.d(TAG, "Didn't find shape overlay for default theme, will use system default"); |
| mOverlayProvider.addSystemDefaultShape(builder); |
| } |
| |
| List<ShapeAppIcon> icons = new ArrayList<>(); |
| for (String packageName : mOverlayProvider.getShapePreviewIconPackages()) { |
| Drawable icon = null; |
| CharSequence name = null; |
| try { |
| icon = mContext.getPackageManager().getApplicationIcon(packageName); |
| ApplicationInfo appInfo = mContext.getPackageManager() |
| .getApplicationInfo(packageName, /* flag= */ 0); |
| name = mContext.getPackageManager().getApplicationLabel(appInfo); |
| } catch (NameNotFoundException e) { |
| Log.d(TAG, "Couldn't find app " + packageName + ", won't use it for icon shape" |
| + "preview"); |
| } finally { |
| if (icon != null && !TextUtils.isEmpty(name)) { |
| icons.add(new ShapeAppIcon(icon, name)); |
| } |
| } |
| } |
| builder.setShapePreviewIcons(icons); |
| |
| try { |
| String iconAndroidOverlayPackage = getOverlayPackage(ICON_ANDROID_PREFIX, |
| DEFAULT_THEME_NAME); |
| mOverlayProvider.addAndroidIconOverlay(builder, iconAndroidOverlayPackage); |
| } catch (NameNotFoundException | NotFoundException e) { |
| Log.d(TAG, "Didn't find Android icons overlay for default theme, using system default"); |
| mOverlayProvider.addSystemDefaultIcons(builder, ANDROID_PACKAGE, ICONS_FOR_PREVIEW); |
| } |
| |
| try { |
| String iconSysUiOverlayPackage = getOverlayPackage(ICON_SYSUI_PREFIX, |
| DEFAULT_THEME_NAME); |
| mOverlayProvider.addSysUiIconOverlay(builder, iconSysUiOverlayPackage); |
| } catch (NameNotFoundException | NotFoundException e) { |
| Log.d(TAG, |
| "Didn't find SystemUi icons overlay for default theme, using system default"); |
| mOverlayProvider.addSystemDefaultIcons(builder, SYSUI_PACKAGE, ICONS_FOR_PREVIEW); |
| } |
| |
| mThemes.add(builder.build(mContext)); |
| } |
| |
| @Override |
| public void storeCustomTheme(CustomTheme theme) { |
| if (mThemes == null) { |
| fetch(options -> { |
| addCustomThemeAndStore(theme); |
| }, false); |
| } else { |
| addCustomThemeAndStore(theme); |
| } |
| } |
| |
| private void addCustomThemeAndStore(CustomTheme theme) { |
| if (!mThemes.contains(theme)) { |
| mThemes.add(theme); |
| } else { |
| mThemes.replaceAll(t -> theme.equals(t) ? theme : t); |
| } |
| JSONArray themesArray = new JSONArray(); |
| mThemes.stream() |
| .filter(themeBundle -> themeBundle instanceof CustomTheme |
| && !themeBundle.getPackagesByCategory().isEmpty()) |
| .forEachOrdered(themeBundle -> addThemeBundleToArray(themesArray, themeBundle)); |
| mCustomizationPreferences.storeCustomThemes(themesArray.toString()); |
| } |
| |
| private void addThemeBundleToArray(JSONArray themesArray, ThemeBundle themeBundle) { |
| JSONObject jsonPackages = themeBundle.getJsonPackages(false); |
| try { |
| jsonPackages.put(THEME_TITLE_FIELD, themeBundle.getTitle()); |
| if (themeBundle instanceof CustomTheme) { |
| jsonPackages.put(THEME_ID_FIELD, ((CustomTheme)themeBundle).getId()); |
| } |
| } catch (JSONException e) { |
| Log.w("Exception saving theme's title", e); |
| } |
| themesArray.put(jsonPackages); |
| } |
| |
| @Override |
| public void removeCustomTheme(CustomTheme theme) { |
| JSONArray themesArray = new JSONArray(); |
| mThemes.stream() |
| .filter(themeBundle -> themeBundle instanceof CustomTheme |
| && ((CustomTheme) themeBundle).isDefined()) |
| .forEachOrdered(customTheme -> { |
| if (!customTheme.equals(theme)) { |
| addThemeBundleToArray(themesArray, customTheme); |
| } |
| }); |
| mCustomizationPreferences.storeCustomThemes(themesArray.toString()); |
| } |
| |
| private void addCustomThemes() { |
| String serializedThemes = mCustomizationPreferences.getSerializedCustomThemes(); |
| int customThemesCount = 0; |
| if (!TextUtils.isEmpty(serializedThemes)) { |
| try { |
| JSONArray customThemes = new JSONArray(serializedThemes); |
| for (int i = 0; i < customThemes.length(); i++) { |
| JSONObject jsonTheme = customThemes.getJSONObject(i); |
| CustomTheme.Builder builder = new CustomTheme.Builder(); |
| try { |
| convertJsonToBuilder(jsonTheme, builder); |
| } catch (NameNotFoundException | NotFoundException e) { |
| Log.i(TAG, "Couldn't parse serialized custom theme", e); |
| builder = null; |
| } |
| if (builder != null) { |
| if (TextUtils.isEmpty(builder.getTitle())) { |
| builder.setTitle(mContext.getString(R.string.custom_theme_title, |
| customThemesCount + 1)); |
| } |
| mThemes.add(builder.build(mContext)); |
| } else { |
| Log.w(TAG, "Couldn't read stored custom theme, resetting"); |
| mThemes.add(new CustomTheme.Builder() |
| .setId(CustomTheme.newId()) |
| .setTitle(mContext.getString( |
| R.string.custom_theme_title, customThemesCount + 1)) |
| .build(mContext)); |
| } |
| customThemesCount++; |
| } |
| } catch (JSONException e) { |
| Log.w(TAG, "Couldn't read stored custom theme, resetting", e); |
| mThemes.add(new CustomTheme.Builder() |
| .setId(CustomTheme.newId()) |
| .setTitle(mContext.getString( |
| R.string.custom_theme_title, customThemesCount + 1)) |
| .build(mContext)); |
| } |
| } |
| } |
| |
| @Nullable |
| @Override |
| public ThemeBundle.Builder parseThemeBundle(String serializedTheme) throws JSONException { |
| JSONObject theme = new JSONObject(serializedTheme); |
| try { |
| ThemeBundle.Builder builder = new ThemeBundle.Builder(); |
| convertJsonToBuilder(theme, builder); |
| return builder; |
| } catch (NameNotFoundException | NotFoundException e) { |
| Log.i(TAG, "Couldn't parse serialized custom theme", e); |
| return null; |
| } |
| } |
| |
| @Nullable |
| @Override |
| public CustomTheme.Builder parseCustomTheme(String serializedTheme) throws JSONException { |
| JSONObject theme = new JSONObject(serializedTheme); |
| try { |
| CustomTheme.Builder builder = new CustomTheme.Builder(); |
| convertJsonToBuilder(theme, builder); |
| return builder; |
| } catch (NameNotFoundException | NotFoundException e) { |
| Log.i(TAG, "Couldn't parse serialized custom theme", e); |
| return null; |
| } |
| } |
| |
| private void convertJsonToBuilder(JSONObject theme, ThemeBundle.Builder builder) |
| throws JSONException, NameNotFoundException, NotFoundException { |
| Map<String, String> customPackages = new HashMap<>(); |
| Iterator<String> keysIterator = theme.keys(); |
| |
| while (keysIterator.hasNext()) { |
| String category = keysIterator.next(); |
| customPackages.put(category, theme.getString(category)); |
| } |
| mOverlayProvider.addShapeOverlay(builder, |
| customPackages.get(OVERLAY_CATEGORY_SHAPE)); |
| mOverlayProvider.addFontOverlay(builder, |
| customPackages.get(OVERLAY_CATEGORY_FONT)); |
| mOverlayProvider.addColorOverlay(builder, |
| customPackages.get(OVERLAY_CATEGORY_COLOR)); |
| mOverlayProvider.addAndroidIconOverlay(builder, |
| customPackages.get(OVERLAY_CATEGORY_ICON_ANDROID)); |
| mOverlayProvider.addSysUiIconOverlay(builder, |
| customPackages.get(OVERLAY_CATEGORY_ICON_SYSUI)); |
| mOverlayProvider.addNoPreviewIconOverlay(builder, |
| customPackages.get(OVERLAY_CATEGORY_ICON_SETTINGS)); |
| mOverlayProvider.addNoPreviewIconOverlay(builder, |
| customPackages.get(OVERLAY_CATEGORY_ICON_LAUNCHER)); |
| mOverlayProvider.addNoPreviewIconOverlay(builder, |
| customPackages.get(OVERLAY_CATEGORY_ICON_THEMEPICKER)); |
| if (theme.has(THEME_TITLE_FIELD)) { |
| builder.setTitle(theme.getString(THEME_TITLE_FIELD)); |
| } |
| if (builder instanceof CustomTheme.Builder && theme.has(THEME_ID_FIELD)) { |
| ((CustomTheme.Builder) builder).setId(theme.getString(THEME_ID_FIELD)); |
| } |
| } |
| |
| @Override |
| public ThemeBundle findEquivalent(ThemeBundle other) { |
| if (mThemes == null) { |
| return null; |
| } |
| for (ThemeBundle theme : mThemes) { |
| if (theme.isEquivalent(other)) { |
| return theme; |
| } |
| } |
| return null; |
| } |
| |
| private String getOverlayPackage(String prefix, String themeName) { |
| return getItemStringFromStub(prefix, themeName); |
| } |
| |
| private ResourceAsset getDrawableResourceAsset(String prefix, String themeName) { |
| int drawableResId = mStubApkResources.getIdentifier(prefix + themeName, |
| "drawable", mStubPackageName); |
| return drawableResId == 0 ? null : new ResourceAsset(mStubApkResources, drawableResId, |
| RequestOptions.fitCenterTransform()); |
| } |
| |
| private ThemeBundleThumbAsset getThumbAsset(String prefix, String themeName) { |
| int drawableResId = mStubApkResources.getIdentifier(prefix + themeName, |
| "drawable", mStubPackageName); |
| return drawableResId == 0 ? null : new ThemeBundleThumbAsset(mStubApkResources, |
| drawableResId); |
| } |
| } |