| /* |
| * 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 android.content.res.Resources.ID_NULL; |
| |
| 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.ComponentName; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.pm.PackageManager; |
| import android.content.pm.PackageManager.NameNotFoundException; |
| import android.content.pm.ResolveInfo; |
| import android.content.res.Resources.NotFoundException; |
| import android.service.wallpaper.WallpaperService; |
| 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.custom.CustomTheme; |
| import com.android.customization.module.CustomizationPreferences; |
| import com.android.wallpaper.R; |
| import com.android.wallpaper.asset.ResourceAsset; |
| import com.android.wallpaper.model.LiveWallpaperInfo; |
| |
| 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 org.xmlpull.v1.XmlPullParserException; |
| |
| import java.io.IOException; |
| 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 WALLPAPER_PREFIX = "theme_wallpaper_"; |
| private static final String WALLPAPER_TITLE_PREFIX = "theme_wallpaper_title_"; |
| private static final String WALLPAPER_ATTRIBUTION_PREFIX = "theme_wallpaper_attribution_"; |
| private static final String WALLPAPER_THUMB_PREFIX = "theme_wallpaper_thumbnail_"; |
| private static final String WALLPAPER_ACTION_PREFIX = "theme_wallpaper_action_"; |
| private static final String WALLPAPER_OPTIONS_PREFIX = "theme_wallpaper_options_"; |
| |
| 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"; |
| |
| // Maximum number of themes allowed (including default, pre-bundled and custom) |
| private static final int MAX_TOTAL_THEMES = 10; |
| |
| 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() { |
| 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); |
| |
| addWallpaper(themeName, builder); |
| |
| 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(); |
| } |
| |
| private void addWallpaper(String themeName, Builder builder) { |
| try { |
| String wallpaperResName = WALLPAPER_PREFIX + themeName; |
| int wallpaperResId = mStubApkResources.getIdentifier(wallpaperResName, |
| "drawable", mStubPackageName); |
| // Check in case the theme has a separate thumbnail for the wallpaper |
| String wallpaperThumbnailResName = WALLPAPER_THUMB_PREFIX + themeName; |
| int wallpaperThumbnailResId = mStubApkResources.getIdentifier(wallpaperThumbnailResName, |
| "drawable", mStubPackageName); |
| if (wallpaperResId != ID_NULL) { |
| builder.setWallpaperInfo(mStubPackageName, wallpaperResName, |
| themeName, wallpaperResId, |
| mStubApkResources.getIdentifier(WALLPAPER_TITLE_PREFIX + themeName, |
| "string", mStubPackageName), |
| mStubApkResources.getIdentifier( |
| WALLPAPER_ATTRIBUTION_PREFIX + themeName, "string", |
| mStubPackageName), |
| mStubApkResources.getIdentifier(WALLPAPER_ACTION_PREFIX + themeName, |
| "string", mStubPackageName)) |
| .setWallpaperAsset(wallpaperThumbnailResId != ID_NULL ? |
| getThumbAsset(WALLPAPER_THUMB_PREFIX, themeName) |
| : getDrawableResourceAsset(WALLPAPER_PREFIX, themeName)); |
| } else { |
| // Try to see if it's a live wallpaper reference |
| wallpaperResId = mStubApkResources.getIdentifier(wallpaperResName, |
| "string", mStubPackageName); |
| if (wallpaperResId != ID_NULL) { |
| String wpComponent = mStubApkResources.getString(wallpaperResId); |
| |
| int wallpaperOptionsResId = mStubApkResources.getIdentifier( |
| WALLPAPER_OPTIONS_PREFIX + themeName, "string", mStubPackageName); |
| String wallpaperOptions = wallpaperOptionsResId != ID_NULL |
| ? mStubApkResources.getString(wallpaperOptionsResId) : null; |
| |
| String[] componentParts = wpComponent.split("/"); |
| Intent liveWpIntent = new Intent(WallpaperService.SERVICE_INTERFACE); |
| liveWpIntent.setComponent( |
| new ComponentName(componentParts[0], componentParts[1])); |
| |
| Context appContext = mContext.getApplicationContext(); |
| PackageManager pm = appContext.getPackageManager(); |
| ResolveInfo resolveInfo = |
| pm.resolveService(liveWpIntent, PackageManager.GET_META_DATA); |
| if (resolveInfo != null) { |
| android.app.WallpaperInfo wallpaperInfo; |
| try { |
| wallpaperInfo = new android.app.WallpaperInfo(appContext, resolveInfo); |
| LiveWallpaperInfo liveInfo = new LiveWallpaperInfo(wallpaperInfo); |
| builder.setLiveWallpaperInfo(liveInfo).setWallpaperAsset( |
| wallpaperThumbnailResId != ID_NULL ? |
| getThumbAsset(WALLPAPER_THUMB_PREFIX, themeName) |
| : liveInfo.getThumbAsset(mContext)) |
| .setWallpaperOptions(wallpaperOptions); |
| } catch (XmlPullParserException | IOException e) { |
| Log.w(TAG, "Skipping wallpaper " + resolveInfo.serviceInfo, e); |
| } |
| } |
| } |
| } |
| } catch (NotFoundException e) { |
| // Nothing to do here, if there's no wallpaper we'll just omit wallpaper |
| } |
| } |
| |
| /** |
| * 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); |
| } |
| for (String packageName : mOverlayProvider.getShapePreviewIconPackages()) { |
| try { |
| builder.addShapePreviewIcon( |
| mContext.getPackageManager().getApplicationIcon(packageName)); |
| } catch (NameNotFoundException e) { |
| Log.d(TAG, "Couldn't find app " + packageName + ", won't use it for icon shape" |
| + "preview"); |
| } |
| } |
| |
| 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); |
| } |
| |
| addWallpaper(DEFAULT_THEME_NAME, builder); |
| |
| 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); |
| ThemeBundle.Builder builder = convertJsonToBuilder(jsonTheme); |
| 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(CustomTheme.newId(), |
| mContext.getString(R.string.custom_theme_title, |
| customThemesCount + 1), new HashMap<>(), null)); |
| } |
| customThemesCount++; |
| } |
| } catch (JSONException e) { |
| Log.w(TAG, "Couldn't read stored custom theme, resetting", e); |
| mThemes.add(new CustomTheme(CustomTheme.newId(), mContext.getString( |
| R.string.custom_theme_title, customThemesCount + 1), |
| new HashMap<>(), null)); |
| } |
| } |
| |
| if (mThemes.size() < MAX_TOTAL_THEMES) { |
| // Add an empty one at the end. |
| mThemes.add(new CustomTheme(CustomTheme.newId(), mContext.getString( |
| R.string.custom_theme_title, customThemesCount + 1), new HashMap<>(), null)); |
| } |
| |
| } |
| |
| @Override |
| public CustomTheme.Builder parseCustomTheme(String serializedTheme) throws JSONException { |
| JSONObject theme = new JSONObject(serializedTheme); |
| return convertJsonToBuilder(theme); |
| } |
| |
| @Nullable |
| private CustomTheme.Builder convertJsonToBuilder(JSONObject theme) throws JSONException { |
| try { |
| Map<String, String> customPackages = new HashMap<>(); |
| Iterator<String> keysIterator = theme.keys(); |
| |
| while (keysIterator.hasNext()) { |
| String category = keysIterator.next(); |
| customPackages.put(category, theme.getString(category)); |
| } |
| CustomTheme.Builder builder = new CustomTheme.Builder(); |
| 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 (theme.has(THEME_ID_FIELD)) { |
| builder.setId(theme.getString(THEME_ID_FIELD)); |
| } |
| return builder; |
| } catch (NameNotFoundException | NotFoundException e) { |
| Log.i(TAG, "Couldn't parse serialized custom theme", e); |
| return null; |
| } |
| } |
| |
| |
| @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); |
| } |
| } |