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