/* * 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.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_SHAPE; import static com.android.customization.model.ResourceConstants.PATH_SIZE; import android.content.Context; import android.content.pm.PackageManager; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Path; import android.graphics.Typeface; import android.graphics.drawable.AdaptiveIconDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.ShapeDrawable; import android.graphics.drawable.shapes.PathShape; import android.text.TextUtils; import android.util.Log; import android.view.View; import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.ColorInt; import androidx.annotation.Dimension; import androidx.annotation.Nullable; import androidx.core.graphics.PathParser; import com.android.customization.model.CustomizationManager; import com.android.customization.model.CustomizationOption; import com.android.customization.model.theme.ThemeBundle.PreviewInfo.ShapeAppIcon; import com.android.customization.widget.DynamicAdaptiveIconDrawable; import com.android.wallpaper.R; import com.android.wallpaper.asset.Asset; import com.android.wallpaper.asset.BitmapCachingAsset; import com.android.wallpaper.model.WallpaperInfo; import com.android.wallpaper.util.ResourceUtils; import org.json.JSONException; import org.json.JSONObject; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; /** * Represents a Theme component available in the system as a "persona" bundle. * Note that in this context a Theme is not related to Android's Styles, but it's rather an * abstraction representing a series of overlays to be applied to the system. */ public class ThemeBundle implements CustomizationOption { private static final String TAG = "ThemeBundle"; private final static String EMPTY_JSON = "{}"; private final static String TIMESTAMP_FIELD = "_applied_timestamp"; private final String mTitle; private final PreviewInfo mPreviewInfo; private final boolean mIsDefault; protected final Map mPackagesByCategory; private WallpaperInfo mOverrideWallpaper; private Asset mOverrideWallpaperAsset; private CharSequence mContentDescription; protected ThemeBundle(String title, Map overlayPackages, boolean isDefault, PreviewInfo previewInfo) { mTitle = title; mIsDefault = isDefault; mPreviewInfo = previewInfo; mPackagesByCategory = Collections.unmodifiableMap(removeNullValues(overlayPackages)); } @Override public String getTitle() { return mTitle; } @Override public void bindThumbnailTile(View view) { Resources res = view.getContext().getResources(); ((TextView) view.findViewById(R.id.theme_option_font)).setTypeface( mPreviewInfo.headlineFontFamily); if (mPreviewInfo.shapeDrawable != null) { ((ShapeDrawable) mPreviewInfo.shapeDrawable).getPaint().setColor( mPreviewInfo.resolveAccentColor(res)); ((ImageView) view.findViewById(R.id.theme_option_shape)).setImageDrawable( mPreviewInfo.shapeDrawable); } if (!mPreviewInfo.icons.isEmpty()) { Drawable icon = mPreviewInfo.icons.get(0).getConstantState().newDrawable().mutate(); icon.setTint(ResourceUtils.getColorAttr( view.getContext(), android.R.attr.textColorSecondary)); ((ImageView) view.findViewById(R.id.theme_option_icon)).setImageDrawable( icon); } view.setContentDescription(getContentDescription(view.getContext())); } @Override public boolean isActive(CustomizationManager manager) { ThemeManager themeManager = (ThemeManager) manager; if (mIsDefault) { String serializedOverlays = themeManager.getStoredOverlays(); return TextUtils.isEmpty(serializedOverlays) || EMPTY_JSON.equals(serializedOverlays); } else { Map currentOverlays = themeManager.getCurrentOverlays(); return mPackagesByCategory.equals(currentOverlays); } } @Override public int getLayoutResId() { return R.layout.theme_option; } /** * This is similar to #equals() but it only compares this theme's packages with the other, that * is, it will return true if applying this theme has the same effect of applying the given one. */ public boolean isEquivalent(ThemeBundle other) { if (other == null) { return false; } if (mIsDefault) { return other.isDefault() || TextUtils.isEmpty(other.getSerializedPackages()) || EMPTY_JSON.equals(other.getSerializedPackages()); } // Map#equals ensures keys and values are compared. return mPackagesByCategory.equals(other.mPackagesByCategory); } public PreviewInfo getPreviewInfo() { return mPreviewInfo; } public void setOverrideThemeWallpaper(WallpaperInfo homeWallpaper) { mOverrideWallpaper = homeWallpaper; mOverrideWallpaperAsset = null; } private Asset getOverrideWallpaperAsset(Context context) { if (mOverrideWallpaperAsset == null) { mOverrideWallpaperAsset = new BitmapCachingAsset(context, mOverrideWallpaper.getThumbAsset(context)); } return mOverrideWallpaperAsset; } boolean isDefault() { return mIsDefault; } public Map getPackagesByCategory() { return mPackagesByCategory; } public String getSerializedPackages() { return getJsonPackages(false).toString(); } public String getSerializedPackagesWithTimestamp() { return getJsonPackages(true).toString(); } JSONObject getJsonPackages(boolean insertTimestamp) { if (isDefault()) { return new JSONObject(); } JSONObject json = new JSONObject(mPackagesByCategory); // Remove items with null values to avoid deserialization issues. removeNullValues(json); if (insertTimestamp) { try { json.put(TIMESTAMP_FIELD, System.currentTimeMillis()); } catch (JSONException e) { Log.e(TAG, "Couldn't add timestamp to serialized themebundle"); } } return json; } private void removeNullValues(JSONObject json) { Iterator keys = json.keys(); Set keysToRemove = new HashSet<>(); while(keys.hasNext()) { String key = keys.next(); if (json.isNull(key)) { keysToRemove.add(key); } } for (String key : keysToRemove) { json.remove(key); } } private Map removeNullValues(Map map) { return map.entrySet() .stream() .filter(entry -> entry.getValue() != null) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); } protected CharSequence getContentDescription(Context context) { if (mContentDescription == null) { CharSequence defaultName = context.getString(R.string.default_theme_title); if (isDefault()) { mContentDescription = defaultName; } else { PackageManager pm = context.getPackageManager(); CharSequence fontName = getOverlayName(pm, OVERLAY_CATEGORY_FONT); CharSequence iconName = getOverlayName(pm, OVERLAY_CATEGORY_ICON_ANDROID); CharSequence shapeName = getOverlayName(pm, OVERLAY_CATEGORY_SHAPE); CharSequence colorName = getOverlayName(pm, OVERLAY_CATEGORY_COLOR); mContentDescription = context.getString(R.string.theme_description, TextUtils.isEmpty(fontName) ? defaultName : fontName, TextUtils.isEmpty(iconName) ? defaultName : iconName, TextUtils.isEmpty(shapeName) ? defaultName : shapeName, TextUtils.isEmpty(colorName) ? defaultName : colorName); } } return mContentDescription; } private CharSequence getOverlayName(PackageManager pm, String overlayCategoryFont) { try { return pm.getApplicationInfo( mPackagesByCategory.get(overlayCategoryFont), 0).loadLabel(pm); } catch (PackageManager.NameNotFoundException e) { return ""; } } public static class PreviewInfo { public final Typeface bodyFontFamily; public final Typeface headlineFontFamily; @ColorInt public final int colorAccentLight; @ColorInt public final int colorAccentDark; public final List icons; public final Drawable shapeDrawable; public final List shapeAppIcons; @Dimension public final int bottomSheeetCornerRadius; /** A class to represent an App icon and its name. */ public static class ShapeAppIcon { private Drawable mIconDrawable; private CharSequence mAppName; public ShapeAppIcon(Drawable icon, CharSequence appName) { mIconDrawable = icon; mAppName = appName; } /** Returns a copy of app icon drawable. */ public Drawable getDrawableCopy() { return mIconDrawable.getConstantState().newDrawable().mutate(); } /** Returns the app name. */ public CharSequence getAppName() { return mAppName; } } private PreviewInfo(Context context, Typeface bodyFontFamily, Typeface headlineFontFamily, int colorAccentLight, int colorAccentDark, List icons, Drawable shapeDrawable, @Dimension int cornerRadius, List shapeAppIcons) { this.bodyFontFamily = bodyFontFamily; this.headlineFontFamily = headlineFontFamily; this.colorAccentLight = colorAccentLight; this.colorAccentDark = colorAccentDark; this.icons = icons; this.shapeDrawable = shapeDrawable; this.bottomSheeetCornerRadius = cornerRadius; this.shapeAppIcons = shapeAppIcons; } /** * Returns the accent color to be applied corresponding with the current configuration's * UI mode. * @return one of {@link #colorAccentDark} or {@link #colorAccentLight} */ @ColorInt public int resolveAccentColor(Resources res) { return (res.getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES ? colorAccentDark : colorAccentLight; } } public static class Builder { protected String mTitle; private Typeface mBodyFontFamily; private Typeface mHeadlineFontFamily; @ColorInt private int mColorAccentLight = -1; @ColorInt private int mColorAccentDark = -1; private List mIcons = new ArrayList<>(); private String mPathString; private Path mShapePath; private boolean mIsDefault; @Dimension private int mCornerRadius; protected Map mPackages = new HashMap<>(); private List mAppIcons = new ArrayList<>(); public ThemeBundle build(Context context) { return new ThemeBundle(mTitle, mPackages, mIsDefault, createPreviewInfo(context)); } public PreviewInfo createPreviewInfo(Context context) { ShapeDrawable shapeDrawable = null; List shapeIcons = new ArrayList<>(); Path path = mShapePath; if (!TextUtils.isEmpty(mPathString)) { path = PathParser.createPathFromPathData(mPathString); } if (path != null) { PathShape shape = new PathShape(path, PATH_SIZE, PATH_SIZE); shapeDrawable = new ShapeDrawable(shape); shapeDrawable.setIntrinsicHeight((int) PATH_SIZE); shapeDrawable.setIntrinsicWidth((int) PATH_SIZE); for (ShapeAppIcon icon : mAppIcons) { Drawable drawable = icon.mIconDrawable; if (drawable instanceof AdaptiveIconDrawable) { AdaptiveIconDrawable adaptiveIcon = (AdaptiveIconDrawable) drawable; shapeIcons.add(new ShapeAppIcon( new DynamicAdaptiveIconDrawable(adaptiveIcon.getBackground(), adaptiveIcon.getForeground(), path), icon.getAppName())); } else if (drawable instanceof DynamicAdaptiveIconDrawable) { shapeIcons.add(icon); } // TODO: add iconloader library's legacy treatment helper methods for // non-adaptive icons } } return new PreviewInfo(context, mBodyFontFamily, mHeadlineFontFamily, mColorAccentLight, mColorAccentDark, mIcons, shapeDrawable, mCornerRadius, shapeIcons); } public Map getPackages() { return Collections.unmodifiableMap(mPackages); } public String getTitle() { return mTitle; } public Builder setTitle(String title) { mTitle = title; return this; } public Builder setBodyFontFamily(@Nullable Typeface bodyFontFamily) { mBodyFontFamily = bodyFontFamily; return this; } public Builder setHeadlineFontFamily(@Nullable Typeface headlineFontFamily) { mHeadlineFontFamily = headlineFontFamily; return this; } public Builder setColorAccentLight(@ColorInt int colorAccentLight) { mColorAccentLight = colorAccentLight; return this; } public Builder setColorAccentDark(@ColorInt int colorAccentDark) { mColorAccentDark = colorAccentDark; return this; } public Builder addIcon(Drawable icon) { mIcons.add(icon); return this; } public Builder addOverlayPackage(String category, String packageName) { mPackages.put(category, packageName); return this; } public Builder setShapePath(String path) { mPathString = path; return this; } public Builder setShapePath(Path path) { mShapePath = path; return this; } public Builder asDefault() { mIsDefault = true; return this; } public Builder setShapePreviewIcons(List appIcons) { mAppIcons.clear(); mAppIcons.addAll(appIcons); return this; } public Builder setBottomSheetCornerRadius(@Dimension int radius) { mCornerRadius = radius; return this; } } }