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