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.ANDROID_PACKAGE;
19 import static com.android.customization.model.ResourceConstants.ICONS_FOR_PREVIEW;
20 import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_COLOR;
21 import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_FONT;
22 import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_ICON_ANDROID;
23 import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_ICON_LAUNCHER;
24 import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_ICON_SETTINGS;
25 import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_ICON_SYSUI;
26 import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_ICON_THEMEPICKER;
27 import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_SHAPE;
28 import static com.android.customization.model.ResourceConstants.SYSUI_PACKAGE;
29 
30 import android.content.Context;
31 import android.content.pm.ApplicationInfo;
32 import android.content.pm.PackageManager.NameNotFoundException;
33 import android.content.res.Resources.NotFoundException;
34 import android.graphics.drawable.Drawable;
35 import android.text.TextUtils;
36 import android.util.Log;
37 
38 import androidx.annotation.Nullable;
39 
40 import com.android.customization.model.CustomizationManager.OptionsFetchedListener;
41 import com.android.customization.model.ResourcesApkProvider;
42 import com.android.customization.model.theme.ThemeBundle.Builder;
43 import com.android.customization.model.theme.ThemeBundle.PreviewInfo.ShapeAppIcon;
44 import com.android.customization.model.theme.custom.CustomTheme;
45 import com.android.customization.module.CustomizationPreferences;
46 import com.android.wallpaper.R;
47 import com.android.wallpaper.asset.ResourceAsset;
48 
49 import com.bumptech.glide.request.RequestOptions;
50 import com.google.android.apps.wallpaper.asset.ThemeBundleThumbAsset;
51 
52 import org.json.JSONArray;
53 import org.json.JSONException;
54 import org.json.JSONObject;
55 
56 import java.util.ArrayList;
57 import java.util.HashMap;
58 import java.util.Iterator;
59 import java.util.List;
60 import java.util.Map;
61 
62 /**
63  * Default implementation of {@link ThemeBundleProvider} that reads Themes' overlays from a stub APK.
64  */
65 public class DefaultThemeProvider extends ResourcesApkProvider implements ThemeBundleProvider {
66 
67     private static final String TAG = "DefaultThemeProvider";
68 
69     private static final String THEMES_ARRAY = "themes";
70     private static final String TITLE_PREFIX = "theme_title_";
71     private static final String FONT_PREFIX = "theme_overlay_font_";
72     private static final String COLOR_PREFIX = "theme_overlay_color_";
73     private static final String SHAPE_PREFIX = "theme_overlay_shape_";
74     private static final String ICON_ANDROID_PREFIX = "theme_overlay_icon_android_";
75     private static final String ICON_LAUNCHER_PREFIX = "theme_overlay_icon_launcher_";
76     private static final String ICON_THEMEPICKER_PREFIX = "theme_overlay_icon_themepicker_";
77     private static final String ICON_SETTINGS_PREFIX = "theme_overlay_icon_settings_";
78     private static final String ICON_SYSUI_PREFIX = "theme_overlay_icon_sysui_";
79 
80     private static final String DEFAULT_THEME_NAME= "default";
81     private static final String THEME_TITLE_FIELD = "_theme_title";
82     private static final String THEME_ID_FIELD = "_theme_id";
83 
84     private final OverlayThemeExtractor mOverlayProvider;
85     private List<ThemeBundle> mThemes;
86     private final CustomizationPreferences mCustomizationPreferences;
87 
DefaultThemeProvider(Context context, CustomizationPreferences customizationPrefs)88     public DefaultThemeProvider(Context context, CustomizationPreferences customizationPrefs) {
89         super(context, context.getString(R.string.themes_stub_package));
90         mOverlayProvider = new OverlayThemeExtractor(context);
91         mCustomizationPreferences = customizationPrefs;
92     }
93 
94     @Override
fetch(OptionsFetchedListener<ThemeBundle> callback, boolean reload)95     public void fetch(OptionsFetchedListener<ThemeBundle> callback, boolean reload) {
96         if (mThemes == null || reload) {
97             mThemes = new ArrayList<>();
98             loadAll();
99         }
100 
101         if(callback != null) {
102             callback.onOptionsLoaded(mThemes);
103         }
104     }
105 
106     @Override
isAvailable()107     public boolean isAvailable() {
108         return mOverlayProvider.isAvailable() && super.isAvailable();
109     }
110 
loadAll()111     private void loadAll() {
112         // Add "Custom" option at the beginning.
113         mThemes.add(new CustomTheme.Builder()
114                 .setId(CustomTheme.newId())
115                 .setTitle(mContext.getString(R.string.custom_theme))
116                 .build(mContext));
117 
118         addDefaultTheme();
119 
120         String[] themeNames = getItemsFromStub(THEMES_ARRAY);
121 
122         for (String themeName : themeNames) {
123             // Default theme needs special treatment (see #addDefaultTheme())
124             if (DEFAULT_THEME_NAME.equals(themeName)) {
125                 continue;
126             }
127             ThemeBundle.Builder builder = new Builder();
128             try {
129                 builder.setTitle(mStubApkResources.getString(
130                         mStubApkResources.getIdentifier(TITLE_PREFIX + themeName,
131                                 "string", mStubPackageName)));
132 
133                 String shapeOverlayPackage = getOverlayPackage(SHAPE_PREFIX, themeName);
134                 mOverlayProvider.addShapeOverlay(builder, shapeOverlayPackage);
135 
136                 String fontOverlayPackage = getOverlayPackage(FONT_PREFIX, themeName);
137                 mOverlayProvider.addFontOverlay(builder, fontOverlayPackage);
138 
139                 String colorOverlayPackage = getOverlayPackage(COLOR_PREFIX, themeName);
140                 mOverlayProvider.addColorOverlay(builder, colorOverlayPackage);
141 
142                 String iconAndroidOverlayPackage = getOverlayPackage(ICON_ANDROID_PREFIX,
143                         themeName);
144 
145                 mOverlayProvider.addAndroidIconOverlay(builder, iconAndroidOverlayPackage);
146 
147                 String iconSysUiOverlayPackage = getOverlayPackage(ICON_SYSUI_PREFIX, themeName);
148 
149                 mOverlayProvider.addSysUiIconOverlay(builder, iconSysUiOverlayPackage);
150 
151                 String iconLauncherOverlayPackage = getOverlayPackage(ICON_LAUNCHER_PREFIX,
152                         themeName);
153                 mOverlayProvider.addNoPreviewIconOverlay(builder, iconLauncherOverlayPackage);
154 
155                 String iconThemePickerOverlayPackage = getOverlayPackage(ICON_THEMEPICKER_PREFIX,
156                         themeName);
157                 mOverlayProvider.addNoPreviewIconOverlay(builder,
158                         iconThemePickerOverlayPackage);
159 
160                 String iconSettingsOverlayPackage = getOverlayPackage(ICON_SETTINGS_PREFIX,
161                         themeName);
162 
163                 mOverlayProvider.addNoPreviewIconOverlay(builder, iconSettingsOverlayPackage);
164 
165                 mThemes.add(builder.build(mContext));
166             } catch (NameNotFoundException | NotFoundException e) {
167                 Log.w(TAG, String.format("Couldn't load part of theme %s, will skip it", themeName),
168                         e);
169             }
170         }
171 
172         addCustomThemes();
173     }
174 
175     /**
176      * Default theme requires different treatment: if there are overlay packages specified in the
177      * stub apk, we'll use those, otherwise we'll get the System default values. But we cannot skip
178      * the default theme.
179      */
addDefaultTheme()180     private void addDefaultTheme() {
181         ThemeBundle.Builder builder = new Builder().asDefault();
182 
183         int titleId = mStubApkResources.getIdentifier(TITLE_PREFIX + DEFAULT_THEME_NAME,
184                 "string", mStubPackageName);
185         if (titleId > 0) {
186             builder.setTitle(mStubApkResources.getString(titleId));
187         } else {
188             builder.setTitle(mContext.getString(R.string.default_theme_title));
189         }
190 
191         try {
192             String colorOverlayPackage = getOverlayPackage(COLOR_PREFIX, DEFAULT_THEME_NAME);
193             mOverlayProvider.addColorOverlay(builder, colorOverlayPackage);
194         } catch (NameNotFoundException | NotFoundException e) {
195             Log.d(TAG, "Didn't find color overlay for default theme, will use system default");
196             mOverlayProvider.addSystemDefaultColor(builder);
197         }
198 
199         try {
200             String fontOverlayPackage = getOverlayPackage(FONT_PREFIX, DEFAULT_THEME_NAME);
201             mOverlayProvider.addFontOverlay(builder, fontOverlayPackage);
202         } catch (NameNotFoundException | NotFoundException e) {
203             Log.d(TAG, "Didn't find font overlay for default theme, will use system default");
204             mOverlayProvider.addSystemDefaultFont(builder);
205         }
206 
207         try {
208             String shapeOverlayPackage = getOverlayPackage(SHAPE_PREFIX, DEFAULT_THEME_NAME);
209             mOverlayProvider.addShapeOverlay(builder ,shapeOverlayPackage, false);
210         } catch (NameNotFoundException | NotFoundException e) {
211             Log.d(TAG, "Didn't find shape overlay for default theme, will use system default");
212             mOverlayProvider.addSystemDefaultShape(builder);
213         }
214 
215         List<ShapeAppIcon> icons = new ArrayList<>();
216         for (String packageName : mOverlayProvider.getShapePreviewIconPackages()) {
217             Drawable icon = null;
218             CharSequence name = null;
219             try {
220                 icon = mContext.getPackageManager().getApplicationIcon(packageName);
221                 ApplicationInfo appInfo = mContext.getPackageManager()
222                         .getApplicationInfo(packageName, /* flag= */ 0);
223                 name = mContext.getPackageManager().getApplicationLabel(appInfo);
224             } catch (NameNotFoundException e) {
225                 Log.d(TAG, "Couldn't find app " + packageName + ", won't use it for icon shape"
226                         + "preview");
227             } finally {
228                 if (icon != null && !TextUtils.isEmpty(name)) {
229                     icons.add(new ShapeAppIcon(icon, name));
230                 }
231             }
232         }
233         builder.setShapePreviewIcons(icons);
234 
235         try {
236             String iconAndroidOverlayPackage = getOverlayPackage(ICON_ANDROID_PREFIX,
237                     DEFAULT_THEME_NAME);
238             mOverlayProvider.addAndroidIconOverlay(builder, iconAndroidOverlayPackage);
239         } catch (NameNotFoundException | NotFoundException e) {
240             Log.d(TAG, "Didn't find Android icons overlay for default theme, using system default");
241             mOverlayProvider.addSystemDefaultIcons(builder, ANDROID_PACKAGE, ICONS_FOR_PREVIEW);
242         }
243 
244         try {
245             String iconSysUiOverlayPackage = getOverlayPackage(ICON_SYSUI_PREFIX,
246                     DEFAULT_THEME_NAME);
247             mOverlayProvider.addSysUiIconOverlay(builder, iconSysUiOverlayPackage);
248         } catch (NameNotFoundException | NotFoundException e) {
249             Log.d(TAG,
250                     "Didn't find SystemUi icons overlay for default theme, using system default");
251             mOverlayProvider.addSystemDefaultIcons(builder, SYSUI_PACKAGE, ICONS_FOR_PREVIEW);
252         }
253 
254         mThemes.add(builder.build(mContext));
255     }
256 
257     @Override
storeCustomTheme(CustomTheme theme)258     public void storeCustomTheme(CustomTheme theme) {
259         if (mThemes == null) {
260             fetch(options -> {
261                 addCustomThemeAndStore(theme);
262             }, false);
263         } else {
264             addCustomThemeAndStore(theme);
265         }
266     }
267 
addCustomThemeAndStore(CustomTheme theme)268     private void addCustomThemeAndStore(CustomTheme theme) {
269         if (!mThemes.contains(theme)) {
270             mThemes.add(theme);
271         } else {
272             mThemes.replaceAll(t -> theme.equals(t) ? theme : t);
273         }
274         JSONArray themesArray = new JSONArray();
275         mThemes.stream()
276                 .filter(themeBundle -> themeBundle instanceof CustomTheme
277                         && !themeBundle.getPackagesByCategory().isEmpty())
278                 .forEachOrdered(themeBundle -> addThemeBundleToArray(themesArray, themeBundle));
279         mCustomizationPreferences.storeCustomThemes(themesArray.toString());
280     }
281 
addThemeBundleToArray(JSONArray themesArray, ThemeBundle themeBundle)282     private void addThemeBundleToArray(JSONArray themesArray, ThemeBundle themeBundle) {
283         JSONObject jsonPackages = themeBundle.getJsonPackages(false);
284         try {
285             jsonPackages.put(THEME_TITLE_FIELD, themeBundle.getTitle());
286             if (themeBundle instanceof CustomTheme) {
287                 jsonPackages.put(THEME_ID_FIELD, ((CustomTheme)themeBundle).getId());
288             }
289         } catch (JSONException e) {
290             Log.w("Exception saving theme's title", e);
291         }
292         themesArray.put(jsonPackages);
293     }
294 
295     @Override
removeCustomTheme(CustomTheme theme)296     public void removeCustomTheme(CustomTheme theme) {
297         JSONArray themesArray = new JSONArray();
298         mThemes.stream()
299                 .filter(themeBundle -> themeBundle instanceof CustomTheme
300                         && ((CustomTheme) themeBundle).isDefined())
301                 .forEachOrdered(customTheme -> {
302                     if (!customTheme.equals(theme)) {
303                         addThemeBundleToArray(themesArray, customTheme);
304                     }
305                 });
306         mCustomizationPreferences.storeCustomThemes(themesArray.toString());
307     }
308 
addCustomThemes()309     private void addCustomThemes() {
310         String serializedThemes = mCustomizationPreferences.getSerializedCustomThemes();
311         int customThemesCount = 0;
312         if (!TextUtils.isEmpty(serializedThemes)) {
313             try {
314                 JSONArray customThemes = new JSONArray(serializedThemes);
315                 for (int i = 0; i < customThemes.length(); i++) {
316                     JSONObject jsonTheme = customThemes.getJSONObject(i);
317                     CustomTheme.Builder builder = new CustomTheme.Builder();
318                     try {
319                         convertJsonToBuilder(jsonTheme, builder);
320                     } catch (NameNotFoundException | NotFoundException e) {
321                         Log.i(TAG, "Couldn't parse serialized custom theme", e);
322                         builder = null;
323                     }
324                     if (builder != null) {
325                         if (TextUtils.isEmpty(builder.getTitle())) {
326                             builder.setTitle(mContext.getString(R.string.custom_theme_title,
327                                     customThemesCount + 1));
328                         }
329                         mThemes.add(builder.build(mContext));
330                     } else {
331                         Log.w(TAG, "Couldn't read stored custom theme, resetting");
332                         mThemes.add(new CustomTheme.Builder()
333                                 .setId(CustomTheme.newId())
334                                 .setTitle(mContext.getString(
335                                         R.string.custom_theme_title, customThemesCount + 1))
336                                 .build(mContext));
337                     }
338                     customThemesCount++;
339                 }
340             } catch (JSONException e) {
341                 Log.w(TAG, "Couldn't read stored custom theme, resetting", e);
342                 mThemes.add(new CustomTheme.Builder()
343                         .setId(CustomTheme.newId())
344                         .setTitle(mContext.getString(
345                                 R.string.custom_theme_title, customThemesCount + 1))
346                         .build(mContext));
347             }
348         }
349     }
350 
351     @Nullable
352     @Override
parseThemeBundle(String serializedTheme)353     public ThemeBundle.Builder parseThemeBundle(String serializedTheme) throws JSONException {
354         JSONObject theme = new JSONObject(serializedTheme);
355         try {
356             ThemeBundle.Builder builder = new ThemeBundle.Builder();
357             convertJsonToBuilder(theme, builder);
358             return builder;
359         } catch (NameNotFoundException | NotFoundException e) {
360             Log.i(TAG, "Couldn't parse serialized custom theme", e);
361             return null;
362         }
363     }
364 
365     @Nullable
366     @Override
parseCustomTheme(String serializedTheme)367     public CustomTheme.Builder parseCustomTheme(String serializedTheme) throws JSONException {
368         JSONObject theme = new JSONObject(serializedTheme);
369         try {
370             CustomTheme.Builder builder = new CustomTheme.Builder();
371             convertJsonToBuilder(theme, builder);
372             return builder;
373         } catch (NameNotFoundException | NotFoundException e) {
374             Log.i(TAG, "Couldn't parse serialized custom theme", e);
375             return null;
376         }
377     }
378 
convertJsonToBuilder(JSONObject theme, ThemeBundle.Builder builder)379     private void convertJsonToBuilder(JSONObject theme, ThemeBundle.Builder builder)
380             throws JSONException, NameNotFoundException, NotFoundException {
381         Map<String, String> customPackages = new HashMap<>();
382         Iterator<String> keysIterator = theme.keys();
383 
384         while (keysIterator.hasNext()) {
385             String category = keysIterator.next();
386             customPackages.put(category, theme.getString(category));
387         }
388         mOverlayProvider.addShapeOverlay(builder,
389                 customPackages.get(OVERLAY_CATEGORY_SHAPE));
390         mOverlayProvider.addFontOverlay(builder,
391                 customPackages.get(OVERLAY_CATEGORY_FONT));
392         mOverlayProvider.addColorOverlay(builder,
393                 customPackages.get(OVERLAY_CATEGORY_COLOR));
394         mOverlayProvider.addAndroidIconOverlay(builder,
395                 customPackages.get(OVERLAY_CATEGORY_ICON_ANDROID));
396         mOverlayProvider.addSysUiIconOverlay(builder,
397                 customPackages.get(OVERLAY_CATEGORY_ICON_SYSUI));
398         mOverlayProvider.addNoPreviewIconOverlay(builder,
399                 customPackages.get(OVERLAY_CATEGORY_ICON_SETTINGS));
400         mOverlayProvider.addNoPreviewIconOverlay(builder,
401                 customPackages.get(OVERLAY_CATEGORY_ICON_LAUNCHER));
402         mOverlayProvider.addNoPreviewIconOverlay(builder,
403                 customPackages.get(OVERLAY_CATEGORY_ICON_THEMEPICKER));
404         if (theme.has(THEME_TITLE_FIELD)) {
405             builder.setTitle(theme.getString(THEME_TITLE_FIELD));
406         }
407         if (builder instanceof CustomTheme.Builder && theme.has(THEME_ID_FIELD)) {
408             ((CustomTheme.Builder) builder).setId(theme.getString(THEME_ID_FIELD));
409         }
410     }
411 
412     @Override
findEquivalent(ThemeBundle other)413     public ThemeBundle findEquivalent(ThemeBundle other) {
414         if (mThemes == null) {
415             return null;
416         }
417         for (ThemeBundle theme : mThemes) {
418             if (theme.isEquivalent(other)) {
419                 return theme;
420             }
421         }
422         return null;
423     }
424 
getOverlayPackage(String prefix, String themeName)425     private String getOverlayPackage(String prefix, String themeName) {
426         return getItemStringFromStub(prefix, themeName);
427     }
428 
getDrawableResourceAsset(String prefix, String themeName)429     private ResourceAsset getDrawableResourceAsset(String prefix, String themeName) {
430         int drawableResId = mStubApkResources.getIdentifier(prefix + themeName,
431                 "drawable", mStubPackageName);
432         return drawableResId == 0 ? null : new ResourceAsset(mStubApkResources, drawableResId,
433                 RequestOptions.fitCenterTransform());
434     }
435 
getThumbAsset(String prefix, String themeName)436     private ThemeBundleThumbAsset getThumbAsset(String prefix, String themeName) {
437         int drawableResId = mStubApkResources.getIdentifier(prefix + themeName,
438                 "drawable", mStubPackageName);
439         return drawableResId == 0 ? null : new ThemeBundleThumbAsset(mStubApkResources,
440                 drawableResId);
441     }
442 }
443