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.OVERLAY_CATEGORY_COLOR;
19 import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_FONT;
20 import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_ICON_ANDROID;
21 import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_SHAPE;
22 import static com.android.customization.model.ResourceConstants.PATH_SIZE;
23 
24 import android.content.Context;
25 import android.content.pm.PackageManager;
26 import android.content.res.Configuration;
27 import android.content.res.Resources;
28 import android.graphics.Path;
29 import android.graphics.Typeface;
30 import android.graphics.drawable.AdaptiveIconDrawable;
31 import android.graphics.drawable.Drawable;
32 import android.graphics.drawable.ShapeDrawable;
33 import android.graphics.drawable.shapes.PathShape;
34 import android.icu.text.SimpleDateFormat;
35 import android.text.TextUtils;
36 import android.util.Log;
37 import android.view.View;
38 import android.widget.ImageView;
39 import android.widget.TextView;
40 
41 import androidx.annotation.ColorInt;
42 import androidx.annotation.Dimension;
43 import androidx.annotation.DrawableRes;
44 import androidx.annotation.Nullable;
45 import androidx.annotation.StringRes;
46 import androidx.core.graphics.PathParser;
47 
48 import com.android.customization.model.CustomizationManager;
49 import com.android.customization.model.CustomizationOption;
50 import com.android.customization.widget.DynamicAdaptiveIconDrawable;
51 import com.android.wallpaper.R;
52 import com.android.wallpaper.asset.Asset;
53 import com.android.wallpaper.asset.BitmapCachingAsset;
54 import com.android.wallpaper.model.LiveWallpaperInfo;
55 import com.android.wallpaper.model.WallpaperInfo;
56 
57 import org.json.JSONException;
58 import org.json.JSONObject;
59 
60 import java.util.ArrayList;
61 import java.util.Collections;
62 import java.util.Date;
63 import java.util.HashMap;
64 import java.util.HashSet;
65 import java.util.Iterator;
66 import java.util.List;
67 import java.util.Locale;
68 import java.util.Map;
69 import java.util.Set;
70 
71 /**
72  * Represents a Theme component available in the system as a "persona" bundle.
73  * Note that in this context a Theme is not related to Android's Styles, but it's rather an
74  * abstraction representing a series of overlays to be applied to the system.
75  */
76 public class ThemeBundle implements CustomizationOption<ThemeBundle> {
77 
78     private static final String TAG = "ThemeBundle";
79     private final static String EMPTY_JSON = "{}";
80     private final static String TIMESTAMP_FIELD = "_applied_timestamp";
81 
82     private final String mTitle;
83     private final PreviewInfo mPreviewInfo;
84     private final boolean mIsDefault;
85     protected final Map<String, String> mPackagesByCategory;
86     @Nullable private final WallpaperInfo mWallpaperInfo;
87     @Nullable private final String mWallpaperOptions;
88     private WallpaperInfo mOverrideWallpaper;
89     private Asset mOverrideWallpaperAsset;
90     private CharSequence mContentDescription;
91 
ThemeBundle(String title, Map<String, String> overlayPackages, boolean isDefault, @Nullable WallpaperInfo wallpaperInfo, @Nullable String wallpaperOptions, PreviewInfo previewInfo)92     protected ThemeBundle(String title, Map<String, String> overlayPackages,
93             boolean isDefault, @Nullable WallpaperInfo wallpaperInfo,
94             @Nullable String wallpaperOptions, PreviewInfo previewInfo) {
95         mTitle = title;
96         mIsDefault = isDefault;
97         mPreviewInfo = previewInfo;
98         mWallpaperInfo = wallpaperInfo;
99         mWallpaperOptions = wallpaperOptions;
100         mPackagesByCategory = Collections.unmodifiableMap(overlayPackages);
101     }
102 
103     @Override
getTitle()104     public String getTitle() {
105         return mTitle;
106     }
107 
108     @Override
bindThumbnailTile(View view)109     public void bindThumbnailTile(View view) {
110         Resources res = view.getContext().getResources();
111 
112         ((TextView) view.findViewById(R.id.theme_option_font)).setTypeface(
113                 mPreviewInfo.headlineFontFamily);
114         if (mPreviewInfo.shapeDrawable != null) {
115             ((ShapeDrawable) mPreviewInfo.shapeDrawable).getPaint().setColor(
116                     mPreviewInfo.resolveAccentColor(res));
117             ((ImageView) view.findViewById(R.id.theme_option_shape)).setImageDrawable(
118                     mPreviewInfo.shapeDrawable);
119         }
120         if (!mPreviewInfo.icons.isEmpty()) {
121             Drawable icon = mPreviewInfo.icons.get(0).getConstantState().newDrawable().mutate();
122             icon.setTint(res.getColor(R.color.icon_thumbnail_color, null));
123             ((ImageView) view.findViewById(R.id.theme_option_icon)).setImageDrawable(
124                     icon);
125         }
126         view.setContentDescription(getContentDescription(view.getContext()));
127     }
128 
129     @Override
isActive(CustomizationManager<ThemeBundle> manager)130     public boolean isActive(CustomizationManager<ThemeBundle> manager) {
131         ThemeManager themeManager = (ThemeManager) manager;
132 
133         if (mIsDefault) {
134             String serializedOverlays = themeManager.getStoredOverlays();
135             return TextUtils.isEmpty(serializedOverlays) || EMPTY_JSON.equals(serializedOverlays);
136         } else {
137             Map<String, String> currentOverlays = themeManager.getCurrentOverlays();
138             return mPackagesByCategory.equals(currentOverlays);
139         }
140     }
141 
142     @Override
getLayoutResId()143     public int getLayoutResId() {
144         return R.layout.theme_option;
145     }
146 
147     /**
148      * This is similar to #equals() but it only compares this theme's packages with the other, that
149      * is, it will return true if applying this theme has the same effect of applying the given one.
150      */
isEquivalent(ThemeBundle other)151     public boolean isEquivalent(ThemeBundle other) {
152         if (other == null) {
153             return false;
154         }
155         if (mIsDefault) {
156             return other.isDefault() || TextUtils.isEmpty(other.getSerializedPackages())
157                     || EMPTY_JSON.equals(other.getSerializedPackages());
158         }
159         // Map#equals ensures keys and values are compared.
160         return mPackagesByCategory.equals(other.mPackagesByCategory);
161     }
162 
getPreviewInfo()163     public PreviewInfo getPreviewInfo() {
164         return mPreviewInfo;
165     }
166 
setOverrideThemeWallpaper(WallpaperInfo homeWallpaper)167     public void setOverrideThemeWallpaper(WallpaperInfo homeWallpaper) {
168         mOverrideWallpaper = homeWallpaper;
169         mOverrideWallpaperAsset = null;
170     }
171 
shouldUseThemeWallpaper()172     public boolean shouldUseThemeWallpaper() {
173         return mOverrideWallpaper == null && mWallpaperInfo != null;
174     }
175 
getWallpaperPreviewAsset(Context context)176     public Asset getWallpaperPreviewAsset(Context context) {
177         return mOverrideWallpaper != null ?
178                 getOverrideWallpaperAsset(context) :
179                 getPreviewInfo().wallpaperAsset;
180     }
181 
getOverrideWallpaperAsset(Context context)182     private Asset getOverrideWallpaperAsset(Context context) {
183         if (mOverrideWallpaperAsset == null) {
184             mOverrideWallpaperAsset = new BitmapCachingAsset(context,
185                     mOverrideWallpaper.getThumbAsset(context));
186         }
187         return mOverrideWallpaperAsset;
188     }
189 
getWallpaperInfo()190     public WallpaperInfo getWallpaperInfo() {
191         return mWallpaperInfo;
192     }
193 
194     @Nullable
getWallpaperOptions()195     public String getWallpaperOptions() {
196         return mWallpaperOptions;
197     }
198 
isDefault()199     boolean isDefault() {
200         return mIsDefault;
201     }
202 
getPackagesByCategory()203     public Map<String, String> getPackagesByCategory() {
204         return mPackagesByCategory;
205     }
206 
getSerializedPackages()207     public String getSerializedPackages() {
208         return getJsonPackages(false).toString();
209     }
210 
getSerializedPackagesWithTimestamp()211     public String getSerializedPackagesWithTimestamp() {
212         return getJsonPackages(true).toString();
213     }
214 
getJsonPackages(boolean insertTimestamp)215     JSONObject getJsonPackages(boolean insertTimestamp) {
216         if (isDefault()) {
217             return new JSONObject();
218         }
219         JSONObject json = new JSONObject(mPackagesByCategory);
220         // Remove items with null values to avoid deserialization issues.
221         removeNullValues(json);
222         if (insertTimestamp) {
223             try {
224                 json.put(TIMESTAMP_FIELD, System.currentTimeMillis());
225             } catch (JSONException e) {
226                 Log.e(TAG, "Couldn't add timestamp to serialized themebundle");
227             }
228         }
229         return json;
230     }
231 
removeNullValues(JSONObject json)232     private void removeNullValues(JSONObject json) {
233         Iterator<String> keys = json.keys();
234         Set<String> keysToRemove = new HashSet<>();
235         while(keys.hasNext()) {
236             String key = keys.next();
237             if (json.isNull(key)) {
238                 keysToRemove.add(key);
239             }
240         }
241         for (String key : keysToRemove) {
242             json.remove(key);
243         }
244     }
245 
getContentDescription(Context context)246     protected CharSequence getContentDescription(Context context) {
247         if (mContentDescription == null) {
248             CharSequence defaultName = context.getString(R.string.default_theme_title);
249             if (isDefault()) {
250                 mContentDescription = defaultName;
251             } else {
252                 PackageManager pm = context.getPackageManager();
253                 CharSequence fontName = getOverlayName(pm, OVERLAY_CATEGORY_FONT);
254                 CharSequence iconName = getOverlayName(pm, OVERLAY_CATEGORY_ICON_ANDROID);
255                 CharSequence shapeName = getOverlayName(pm, OVERLAY_CATEGORY_SHAPE);
256                 CharSequence colorName = getOverlayName(pm, OVERLAY_CATEGORY_COLOR);
257                 mContentDescription = context.getString(R.string.theme_description,
258                         TextUtils.isEmpty(fontName) ? defaultName : fontName,
259                         TextUtils.isEmpty(iconName) ? defaultName : iconName,
260                         TextUtils.isEmpty(shapeName) ? defaultName : shapeName,
261                         TextUtils.isEmpty(colorName) ? defaultName : colorName);
262             }
263         }
264         return mContentDescription;
265     }
266 
getOverlayName(PackageManager pm, String overlayCategoryFont)267     private CharSequence getOverlayName(PackageManager pm, String overlayCategoryFont) {
268         try {
269             return pm.getApplicationInfo(
270                     mPackagesByCategory.get(overlayCategoryFont), 0).loadLabel(pm);
271         } catch (PackageManager.NameNotFoundException e) {
272             return "";
273         }
274     }
275 
276     public static class PreviewInfo {
277         public final Typeface bodyFontFamily;
278         public final Typeface headlineFontFamily;
279         @ColorInt public final int colorAccentLight;
280         @ColorInt public final int colorAccentDark;
281         public final List<Drawable> icons;
282         public final Drawable shapeDrawable;
283         @Nullable public final Asset wallpaperAsset;
284         public final List<Drawable> shapeAppIcons;
285         @Dimension public final int bottomSheeetCornerRadius;
286 
PreviewInfo(Context context, Typeface bodyFontFamily, Typeface headlineFontFamily, int colorAccentLight, int colorAccentDark, List<Drawable> icons, Drawable shapeDrawable, @Dimension int cornerRadius, @Nullable Asset wallpaperAsset, List<Drawable> shapeAppIcons)287         private PreviewInfo(Context context, Typeface bodyFontFamily, Typeface headlineFontFamily,
288                 int colorAccentLight, int colorAccentDark, List<Drawable> icons,
289                 Drawable shapeDrawable, @Dimension int cornerRadius,
290                 @Nullable Asset wallpaperAsset, List<Drawable> shapeAppIcons) {
291             this.bodyFontFamily = bodyFontFamily;
292             this.headlineFontFamily = headlineFontFamily;
293             this.colorAccentLight = colorAccentLight;
294             this.colorAccentDark = colorAccentDark;
295             this.icons = icons;
296             this.shapeDrawable = shapeDrawable;
297             this.bottomSheeetCornerRadius = cornerRadius;
298             this.wallpaperAsset = wallpaperAsset == null
299                     ? null : new BitmapCachingAsset(context, wallpaperAsset);
300             this.shapeAppIcons = shapeAppIcons;
301         }
302 
303         /**
304          * Returns the accent color to be applied corresponding with the current configuration's
305          * UI mode.
306          * @return one of {@link #colorAccentDark} or {@link #colorAccentLight}
307          */
308         @ColorInt
resolveAccentColor(Resources res)309         public int resolveAccentColor(Resources res) {
310             return (res.getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK)
311                     == Configuration.UI_MODE_NIGHT_YES ? colorAccentDark : colorAccentLight;
312         }
313     }
314 
315     public static class Builder {
316         protected String mTitle;
317         private Typeface mBodyFontFamily;
318         private Typeface mHeadlineFontFamily;
319         @ColorInt private int mColorAccentLight = -1;
320         @ColorInt private int mColorAccentDark = -1;
321         private List<Drawable> mIcons = new ArrayList<>();
322         private String mPathString;
323         private Path mShapePath;
324         private boolean mIsDefault;
325         @Dimension private int mCornerRadius;
326         private Asset mWallpaperAsset;
327         private WallpaperInfo mWallpaperInfo;
328         private String mWallpaperOptions;
329         protected Map<String, String> mPackages = new HashMap<>();
330         private List<Drawable> mAppIcons = new ArrayList<>();
331 
build(Context context)332         public ThemeBundle build(Context context) {
333             return new ThemeBundle(mTitle, mPackages, mIsDefault, mWallpaperInfo, mWallpaperOptions,
334                     createPreviewInfo(context));
335         }
336 
createPreviewInfo(Context context)337         public PreviewInfo createPreviewInfo(Context context) {
338             ShapeDrawable shapeDrawable = null;
339             List<Drawable> shapeIcons = new ArrayList<>();
340             Path path = mShapePath;
341             if (!TextUtils.isEmpty(mPathString)) {
342                 path = PathParser.createPathFromPathData(mPathString);
343             }
344             if (path != null) {
345                 PathShape shape = new PathShape(path, PATH_SIZE, PATH_SIZE);
346                 shapeDrawable = new ShapeDrawable(shape);
347                 shapeDrawable.setIntrinsicHeight((int) PATH_SIZE);
348                 shapeDrawable.setIntrinsicWidth((int) PATH_SIZE);
349                 for (Drawable icon : mAppIcons) {
350                     if (icon instanceof AdaptiveIconDrawable) {
351                         AdaptiveIconDrawable adaptiveIcon = (AdaptiveIconDrawable) icon;
352                         shapeIcons.add(new DynamicAdaptiveIconDrawable(adaptiveIcon.getBackground(),
353                                 adaptiveIcon.getForeground(), path));
354                     } else if (icon instanceof DynamicAdaptiveIconDrawable) {
355                         shapeIcons.add(icon);
356                     }
357                     // TODO: add iconloader library's legacy treatment helper methods for
358                     //  non-adaptive icons
359                 }
360             }
361             return new PreviewInfo(context, mBodyFontFamily, mHeadlineFontFamily, mColorAccentLight,
362                     mColorAccentDark, mIcons, shapeDrawable, mCornerRadius,
363                     mWallpaperAsset, shapeIcons);
364         }
365 
getPackages()366         public Map<String, String> getPackages() {
367             return Collections.unmodifiableMap(mPackages);
368         }
369 
getTitle()370         public String getTitle() {
371             return mTitle;
372         }
373 
setTitle(String title)374         public Builder setTitle(String title) {
375             mTitle = title;
376             return this;
377         }
378 
setBodyFontFamily(@ullable Typeface bodyFontFamily)379         public Builder setBodyFontFamily(@Nullable Typeface bodyFontFamily) {
380             mBodyFontFamily = bodyFontFamily;
381             return this;
382         }
383 
setHeadlineFontFamily(@ullable Typeface headlineFontFamily)384         public Builder setHeadlineFontFamily(@Nullable Typeface headlineFontFamily) {
385             mHeadlineFontFamily = headlineFontFamily;
386             return this;
387         }
388 
setColorAccentLight(@olorInt int colorAccentLight)389         public Builder setColorAccentLight(@ColorInt int colorAccentLight) {
390             mColorAccentLight = colorAccentLight;
391             return this;
392         }
393 
setColorAccentDark(@olorInt int colorAccentDark)394         public Builder setColorAccentDark(@ColorInt int colorAccentDark) {
395             mColorAccentDark = colorAccentDark;
396             return this;
397         }
398 
addIcon(Drawable icon)399         public Builder addIcon(Drawable icon) {
400             mIcons.add(icon);
401             return this;
402         }
403 
addOverlayPackage(String category, String packageName)404         public Builder addOverlayPackage(String category, String packageName) {
405             mPackages.put(category, packageName);
406             return this;
407         }
408 
setShapePath(String path)409         public Builder setShapePath(String path) {
410             mPathString = path;
411             return this;
412         }
413 
setShapePath(Path path)414         public Builder setShapePath(Path path) {
415             mShapePath = path;
416             return this;
417         }
418 
setWallpaperInfo(String wallpaperPackageName, String wallpaperResName, String themeId, @DrawableRes int wallpaperResId, @StringRes int titleResId, @StringRes int attributionResId, @StringRes int actionUrlResId)419         public Builder setWallpaperInfo(String wallpaperPackageName, String wallpaperResName,
420                 String themeId, @DrawableRes int wallpaperResId, @StringRes int titleResId,
421                 @StringRes int attributionResId, @StringRes int actionUrlResId) {
422             mWallpaperInfo = new ThemeBundledWallpaperInfo(wallpaperPackageName, wallpaperResName,
423                     themeId, wallpaperResId, titleResId, attributionResId, actionUrlResId);
424             return this;
425         }
426 
setLiveWallpaperInfo(LiveWallpaperInfo info)427         public Builder setLiveWallpaperInfo(LiveWallpaperInfo info) {
428             mWallpaperInfo = info;
429             return this;
430         }
431 
432 
setWallpaperAsset(Asset wallpaperAsset)433         public Builder setWallpaperAsset(Asset wallpaperAsset) {
434             mWallpaperAsset = wallpaperAsset;
435             return this;
436         }
437 
setWallpaperOptions(String wallpaperOptions)438         public Builder setWallpaperOptions(String wallpaperOptions) {
439             mWallpaperOptions = wallpaperOptions;
440             return this;
441         }
442 
asDefault()443         public Builder asDefault() {
444             mIsDefault = true;
445             return this;
446         }
447 
addShapePreviewIcon(Drawable appIcon)448         public Builder addShapePreviewIcon(Drawable appIcon) {
449             mAppIcons.add(appIcon);
450             return this;
451         }
452 
setBottomSheetCornerRadius(@imension int radius)453         public Builder setBottomSheetCornerRadius(@Dimension int radius) {
454             mCornerRadius = radius;
455             return this;
456         }
457     }
458 }
459