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.custom;
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_ICON_LAUNCHER;
22 import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_ICON_SETTINGS;
23 import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_ICON_SYSUI;
24 import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_ICON_THEMEPICKER;
25 import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_SHAPE;
26 import static com.android.customization.model.ResourceConstants.getLauncherPackage;
27 
28 import android.content.Context;
29 import android.content.pm.PackageManager.NameNotFoundException;
30 import android.content.res.ColorStateList;
31 import android.content.res.Configuration;
32 import android.content.res.Resources;
33 import android.content.res.Resources.NotFoundException;
34 import android.content.res.Resources.Theme;
35 import android.content.res.TypedArray;
36 import android.graphics.Path;
37 import android.graphics.Typeface;
38 import android.graphics.drawable.Drawable;
39 import android.graphics.drawable.LayerDrawable;
40 import android.graphics.drawable.ShapeDrawable;
41 import android.graphics.drawable.StateListDrawable;
42 import android.text.TextUtils;
43 import android.view.Gravity;
44 import android.view.LayoutInflater;
45 import android.view.View;
46 import android.view.ViewGroup;
47 import android.widget.CompoundButton;
48 import android.widget.ImageView;
49 import android.widget.SeekBar;
50 import android.widget.Switch;
51 import android.widget.TextView;
52 
53 import androidx.annotation.ColorInt;
54 import androidx.annotation.Dimension;
55 import androidx.annotation.DrawableRes;
56 import androidx.annotation.Nullable;
57 import androidx.annotation.StringRes;
58 import androidx.core.graphics.ColorUtils;
59 
60 import com.android.customization.model.CustomizationManager;
61 import com.android.customization.model.CustomizationOption;
62 import com.android.customization.model.ResourceConstants;
63 import com.android.customization.model.theme.ThemeBundle.PreviewInfo.ShapeAppIcon;
64 import com.android.customization.model.theme.custom.CustomTheme.Builder;
65 import com.android.wallpaper.R;
66 import com.android.wallpaper.util.ResourceUtils;
67 
68 import java.util.ArrayList;
69 import java.util.HashMap;
70 import java.util.List;
71 import java.util.Map;
72 import java.util.Objects;
73 
74 /**
75  * Represents an option of a component of a custom Theme (for example, a possible color, or font,
76  * shape, etc).
77  * Extending classes correspond to each component's options and provide the structure to bind
78  * preview and thumbnails.
79  * // TODO (santie): refactor the logic to bind preview cards to reuse between ThemeFragment and
80  * // here
81  */
82 public abstract class ThemeComponentOption implements CustomizationOption<ThemeComponentOption> {
83 
84     protected final Map<String, String> mOverlayPackageNames = new HashMap<>();
85 
addOverlayPackage(String category, String packageName)86     protected void addOverlayPackage(String category, String packageName) {
87         mOverlayPackageNames.put(category, packageName);
88     }
89 
getOverlayPackages()90     public Map<String, String> getOverlayPackages() {
91         return mOverlayPackageNames;
92     }
93 
94     @Override
getTitle()95     public String getTitle() {
96         return null;
97     }
98 
bindPreview(ViewGroup container)99     public abstract void bindPreview(ViewGroup container);
100 
buildStep(Builder builder)101     public Builder buildStep(Builder builder) {
102         getOverlayPackages().forEach(builder::addOverlayPackage);
103         return builder;
104     }
105 
106     public static class FontOption extends ThemeComponentOption {
107 
108         private final String mLabel;
109         private final Typeface mHeadlineFont;
110         private final Typeface mBodyFont;
111 
FontOption(String packageName, String label, Typeface headlineFont, Typeface bodyFont)112         public FontOption(String packageName, String label, Typeface headlineFont,
113                 Typeface bodyFont) {
114             addOverlayPackage(OVERLAY_CATEGORY_FONT, packageName);
115             mLabel = label;
116             mHeadlineFont = headlineFont;
117             mBodyFont = bodyFont;
118         }
119 
120         @Override
getTitle()121         public String getTitle() {
122             return null;
123         }
124 
125         @Override
bindThumbnailTile(View view)126         public void bindThumbnailTile(View view) {
127             ((TextView) view.findViewById(R.id.thumbnail_text)).setTypeface(
128                     mHeadlineFont);
129             view.setContentDescription(mLabel);
130         }
131 
132         @Override
isActive(CustomizationManager<ThemeComponentOption> manager)133         public boolean isActive(CustomizationManager<ThemeComponentOption> manager) {
134             CustomThemeManager customThemeManager = (CustomThemeManager) manager;
135             return Objects.equals(getOverlayPackages().get(OVERLAY_CATEGORY_FONT),
136                     customThemeManager.getOverlayPackages().get(OVERLAY_CATEGORY_FONT));
137         }
138 
139         @Override
getLayoutResId()140         public int getLayoutResId() {
141             return R.layout.theme_font_option;
142         }
143 
144         @Override
bindPreview(ViewGroup container)145         public void bindPreview(ViewGroup container) {
146             container.setContentDescription(
147                     container.getContext().getString(R.string.font_preview_content_description));
148 
149             bindPreviewHeader(container, R.string.preview_name_font, R.drawable.ic_font, null);
150 
151             ViewGroup cardBody = container.findViewById(R.id.theme_preview_card_body_container);
152             if (cardBody.getChildCount() == 0) {
153                 LayoutInflater.from(container.getContext()).inflate(
154                         R.layout.preview_card_font_content,
155                         cardBody, true);
156             }
157             TextView title = container.findViewById(R.id.font_card_title);
158             title.setTypeface(mHeadlineFont);
159             TextView bodyText = container.findViewById(R.id.font_card_body);
160             bodyText.setTypeface(mBodyFont);
161             container.findViewById(R.id.font_card_divider).setBackgroundColor(
162                     title.getCurrentTextColor());
163         }
164 
165         @Override
buildStep(Builder builder)166         public Builder buildStep(Builder builder) {
167             builder.setHeadlineFontFamily(mHeadlineFont).setBodyFontFamily(mBodyFont);
168             return super.buildStep(builder);
169         }
170     }
171 
bindPreviewHeader(ViewGroup container, @StringRes int headerTextResId, @DrawableRes int headerIcon, String drawableName)172     void bindPreviewHeader(ViewGroup container, @StringRes int headerTextResId,
173             @DrawableRes int headerIcon, String drawableName) {
174         TextView header = container.findViewById(R.id.theme_preview_card_header);
175         header.setText(headerTextResId);
176 
177         Context context = container.getContext();
178         Drawable icon;
179         if (!TextUtils.isEmpty(drawableName)) {
180             try {
181                 Resources resources = context.getPackageManager()
182                         .getResourcesForApplication(getLauncherPackage(context));
183                 icon = resources.getDrawable(resources.getIdentifier(
184                         drawableName, "drawable", getLauncherPackage(context)), null);
185             } catch (NameNotFoundException | NotFoundException e) {
186                 icon = context.getResources().getDrawable(headerIcon, context.getTheme());
187             }
188         } else {
189             icon = context.getResources().getDrawable(headerIcon, context.getTheme());
190         }
191         int size = context.getResources().getDimensionPixelSize(R.dimen.card_header_icon_size);
192         icon.setBounds(0, 0, size, size);
193 
194         header.setCompoundDrawables(null, icon, null, null);
195         header.setCompoundDrawableTintList(ColorStateList.valueOf(
196                 header.getCurrentTextColor()));
197     }
198 
199     public static class IconOption extends ThemeComponentOption {
200 
201         public static final int THUMBNAIL_ICON_POSITION = 0;
202         private static int[] mIconIds = {
203                 R.id.preview_icon_0, R.id.preview_icon_1, R.id.preview_icon_2, R.id.preview_icon_3,
204                 R.id.preview_icon_4, R.id.preview_icon_5
205         };
206 
207         private List<Drawable> mIcons = new ArrayList<>();
208         private String mLabel;
209 
210         @Override
bindThumbnailTile(View view)211         public void bindThumbnailTile(View view) {
212             Resources res = view.getContext().getResources();
213             Drawable icon = mIcons.get(THUMBNAIL_ICON_POSITION)
214                     .getConstantState().newDrawable().mutate();
215             icon.setTint(ResourceUtils.getColorAttr(
216                     view.getContext(), android.R.attr.textColorSecondary));
217             ((ImageView) view.findViewById(R.id.option_icon)).setImageDrawable(
218                     icon);
219             view.setContentDescription(mLabel);
220         }
221 
222         @Override
isActive(CustomizationManager<ThemeComponentOption> manager)223         public boolean isActive(CustomizationManager<ThemeComponentOption> manager) {
224             CustomThemeManager customThemeManager = (CustomThemeManager) manager;
225             Map<String, String> themePackages = customThemeManager.getOverlayPackages();
226             if (getOverlayPackages().isEmpty()) {
227                 return themePackages.get(OVERLAY_CATEGORY_ICON_SYSUI) == null &&
228                         themePackages.get(OVERLAY_CATEGORY_ICON_SETTINGS) == null &&
229                         themePackages.get(OVERLAY_CATEGORY_ICON_ANDROID) == null &&
230                         themePackages.get(OVERLAY_CATEGORY_ICON_LAUNCHER) == null &&
231                         themePackages.get(OVERLAY_CATEGORY_ICON_THEMEPICKER) == null;
232             }
233             for (Map.Entry<String, String> overlayEntry : getOverlayPackages().entrySet()) {
234                 if(!Objects.equals(overlayEntry.getValue(),
235                         themePackages.get(overlayEntry.getKey()))) {
236                     return false;
237                 }
238             }
239             return true;
240         }
241 
242         @Override
getLayoutResId()243         public int getLayoutResId() {
244             return R.layout.theme_icon_option;
245         }
246 
247         @Override
bindPreview(ViewGroup container)248         public void bindPreview(ViewGroup container) {
249             container.setContentDescription(
250                     container.getContext().getString(R.string.icon_preview_content_description));
251 
252             bindPreviewHeader(container, R.string.preview_name_icon, R.drawable.ic_widget,
253                     "ic_widget");
254 
255             ViewGroup cardBody = container.findViewById(R.id.theme_preview_card_body_container);
256             if (cardBody.getChildCount() == 0) {
257                 LayoutInflater.from(container.getContext()).inflate(
258                         R.layout.preview_card_icon_content, cardBody, true);
259             }
260             for (int i = 0; i < mIconIds.length && i < mIcons.size(); i++) {
261                 ((ImageView) container.findViewById(mIconIds[i])).setImageDrawable(
262                         mIcons.get(i));
263             }
264         }
265 
addIcon(Drawable previewIcon)266         public void addIcon(Drawable previewIcon) {
267             mIcons.add(previewIcon);
268         }
269 
270         /**
271          * @return whether this icon option has overlays and previews for all the required packages
272          */
isValid(Context context)273         public boolean isValid(Context context) {
274             return getOverlayPackages().keySet().size() ==
275                     ResourceConstants.getPackagesToOverlay(context).length;
276         }
277 
setLabel(String label)278         public void setLabel(String label) {
279             mLabel = label;
280         }
281 
282         @Override
buildStep(Builder builder)283         public Builder buildStep(Builder builder) {
284             for (Drawable icon : mIcons) {
285                 builder.addIcon(icon);
286             }
287             return super.buildStep(builder);
288         }
289     }
290 
291     public static class ColorOption extends ThemeComponentOption {
292 
293         /**
294          * Ids of views used to represent quick setting tiles in the color preview screen
295          */
296         private static int[] COLOR_TILE_IDS = {
297                 R.id.preview_color_qs_0_bg, R.id.preview_color_qs_1_bg, R.id.preview_color_qs_2_bg
298         };
299 
300         /**
301          * Ids of the views for the foreground of the icon, mapping to the corresponding index of
302          * the actual icon drawable.
303          */
304         static int[][] COLOR_TILES_ICON_IDS = {
305                 new int[]{ R.id.preview_color_qs_0_icon, 0},
306                 new int[]{ R.id.preview_color_qs_1_icon, 1},
307                 new int[] { R.id.preview_color_qs_2_icon, 3}
308         };
309 
310         /**
311          * Ids of views used to represent control buttons in the color preview screen
312          */
313         private static int[] COLOR_BUTTON_IDS = {
314                 R.id.preview_check_selected, R.id.preview_radio_selected,
315                 R.id.preview_toggle_selected
316         };
317 
318         @ColorInt private int mColorAccentLight;
319         @ColorInt private int mColorAccentDark;
320         /**
321          * Icons shown as example of QuickSettings tiles in the color preview screen.
322          */
323         private List<Drawable> mIcons = new ArrayList<>();
324 
325         /**
326          * Drawable with the currently selected shape to be used as background of the sample
327          * QuickSetting icons in the color preview screen.
328          */
329         private Drawable mShapeDrawable;
330 
331         private String mLabel;
332 
ColorOption(String packageName, String label, @ColorInt int lightColor, @ColorInt int darkColor)333         ColorOption(String packageName, String label, @ColorInt int lightColor,
334                 @ColorInt int darkColor) {
335             addOverlayPackage(OVERLAY_CATEGORY_COLOR, packageName);
336             mLabel = label;
337             mColorAccentLight = lightColor;
338             mColorAccentDark = darkColor;
339         }
340 
341         @Override
bindThumbnailTile(View view)342         public void bindThumbnailTile(View view) {
343             @ColorInt int color = resolveColor(view.getResources());
344             LayerDrawable selectedOption = (LayerDrawable) view.getResources().getDrawable(
345                     R.drawable.color_chip_hollow, view.getContext().getTheme());
346             Drawable unselectedOption = view.getResources().getDrawable(
347                     R.drawable.color_chip_filled, view.getContext().getTheme());
348 
349             selectedOption.findDrawableByLayerId(R.id.center_fill).setTintList(
350                     ColorStateList.valueOf(color));
351             unselectedOption.setTintList(ColorStateList.valueOf(color));
352 
353             StateListDrawable stateListDrawable = new StateListDrawable();
354             stateListDrawable.addState(new int[] {android.R.attr.state_activated}, selectedOption);
355             stateListDrawable.addState(
356                     new int[] {-android.R.attr.state_activated}, unselectedOption);
357 
358             ((ImageView) view.findViewById(R.id.option_tile)).setImageDrawable(stateListDrawable);
359             view.setContentDescription(mLabel);
360         }
361 
362         @ColorInt
resolveColor(Resources res)363         private int resolveColor(Resources res) {
364             Configuration configuration = res.getConfiguration();
365             return (configuration.uiMode & Configuration.UI_MODE_NIGHT_MASK)
366                     == Configuration.UI_MODE_NIGHT_YES ? mColorAccentDark : mColorAccentLight;
367         }
368 
369         @Override
isActive(CustomizationManager<ThemeComponentOption> manager)370         public boolean isActive(CustomizationManager<ThemeComponentOption> manager) {
371             CustomThemeManager customThemeManager = (CustomThemeManager) manager;
372             return Objects.equals(getOverlayPackages().get(OVERLAY_CATEGORY_COLOR),
373                     customThemeManager.getOverlayPackages().get(OVERLAY_CATEGORY_COLOR));
374         }
375 
376         @Override
getLayoutResId()377         public int getLayoutResId() {
378             return R.layout.theme_color_option;
379         }
380 
381         @Override
bindPreview(ViewGroup container)382         public void bindPreview(ViewGroup container) {
383             container.setContentDescription(
384                     container.getContext().getString(R.string.color_preview_content_description));
385 
386             bindPreviewHeader(container, R.string.preview_name_color, R.drawable.ic_colorize_24px,
387                     null);
388 
389             ViewGroup cardBody = container.findViewById(R.id.theme_preview_card_body_container);
390             if (cardBody.getChildCount() == 0) {
391                 LayoutInflater.from(container.getContext()).inflate(
392                         R.layout.preview_card_color_content, cardBody, true);
393             }
394             Resources res = container.getResources();
395             @ColorInt int accentColor = resolveColor(res);
396             @ColorInt int controlGreyColor = ResourceUtils.getColorAttr(
397                     container.getContext(),
398                     android.R.attr.textColorTertiary);
399             ColorStateList tintList = new ColorStateList(
400                     new int[][]{
401                             new int[]{android.R.attr.state_selected},
402                             new int[]{android.R.attr.state_checked},
403                             new int[]{-android.R.attr.state_enabled}
404                     },
405                     new int[] {
406                             accentColor,
407                             accentColor,
408                             controlGreyColor
409                     }
410             );
411 
412             for (int i = 0; i < COLOR_BUTTON_IDS.length; i++) {
413                 CompoundButton button = container.findViewById(COLOR_BUTTON_IDS[i]);
414                 button.setButtonTintList(tintList);
415             }
416 
417             Switch enabledSwitch = container.findViewById(R.id.preview_toggle_selected);
418             enabledSwitch.setThumbTintList(tintList);
419             enabledSwitch.setTrackTintList(tintList);
420 
421             ColorStateList seekbarTintList = ColorStateList.valueOf(accentColor);
422             SeekBar seekbar = container.findViewById(R.id.preview_seekbar);
423             seekbar.setThumbTintList(seekbarTintList);
424             seekbar.setProgressTintList(seekbarTintList);
425             seekbar.setProgressBackgroundTintList(seekbarTintList);
426             // Disable seekbar
427             seekbar.setOnTouchListener((view, motionEvent) -> true);
428 
429             int iconFgColor = ResourceUtils.getColorAttr(container.getContext(),
430                     android.R.attr.colorBackground);
431             if (!mIcons.isEmpty() && mShapeDrawable != null) {
432                 for (int i = 0; i < COLOR_TILE_IDS.length; i++) {
433                     Drawable icon = mIcons.get(COLOR_TILES_ICON_IDS[i][1]).getConstantState()
434                             .newDrawable();
435                     icon.setTint(iconFgColor);
436                     //TODO: load and set the shape.
437                     Drawable bgShape = mShapeDrawable.getConstantState().newDrawable();
438                     bgShape.setTint(accentColor);
439 
440                     ImageView bg = container.findViewById(COLOR_TILE_IDS[i]);
441                     bg.setImageDrawable(bgShape);
442                     ImageView fg = container.findViewById(COLOR_TILES_ICON_IDS[i][0]);
443                     fg.setImageDrawable(icon);
444                 }
445             }
446         }
447 
setPreviewIcons(List<Drawable> icons)448         public void setPreviewIcons(List<Drawable> icons) {
449             mIcons.addAll(icons);
450         }
451 
setShapeDrawable(@ullable Drawable shapeDrawable)452         public void setShapeDrawable(@Nullable Drawable shapeDrawable) {
453             mShapeDrawable = shapeDrawable;
454         }
455 
456         @Override
buildStep(Builder builder)457         public Builder buildStep(Builder builder) {
458             builder.setColorAccentDark(mColorAccentDark).setColorAccentLight(mColorAccentLight);
459             return super.buildStep(builder);
460         }
461     }
462 
463     public static class ShapeOption extends ThemeComponentOption {
464 
465         private final LayerDrawable mShape;
466         private final List<ShapeAppIcon> mAppIcons;
467         private final String mLabel;
468         private final Path mPath;
469         private final int mCornerRadius;
470         private int[] mShapeIconIds = {
471                 R.id.shape_preview_icon_0, R.id.shape_preview_icon_1, R.id.shape_preview_icon_2,
472                 R.id.shape_preview_icon_3, R.id.shape_preview_icon_4, R.id.shape_preview_icon_5
473         };
474 
ShapeOption(String packageName, String label, Path path, @Dimension int cornerRadius, Drawable shapeDrawable, List<ShapeAppIcon> appIcons)475         ShapeOption(String packageName, String label, Path path,
476                 @Dimension int cornerRadius, Drawable shapeDrawable,
477                 List<ShapeAppIcon> appIcons) {
478             addOverlayPackage(OVERLAY_CATEGORY_SHAPE, packageName);
479             mLabel = label;
480             mAppIcons = appIcons;
481             mPath = path;
482             mCornerRadius = cornerRadius;
483             Drawable background = shapeDrawable.getConstantState().newDrawable();
484             Drawable foreground = shapeDrawable.getConstantState().newDrawable();
485             mShape = new LayerDrawable(new Drawable[]{background, foreground});
486             mShape.setLayerGravity(0, Gravity.CENTER);
487             mShape.setLayerGravity(1, Gravity.CENTER);
488         }
489 
490         @Override
bindThumbnailTile(View view)491         public void bindThumbnailTile(View view) {
492             ImageView thumb = view.findViewById(R.id.shape_thumbnail);
493             Resources res = view.getResources();
494             Theme theme = view.getContext().getTheme();
495             int borderWidth = 2 * res.getDimensionPixelSize(R.dimen.option_border_width);
496 
497             Drawable background = mShape.getDrawable(0);
498             background.setTintList(res.getColorStateList(R.color.option_border_color, theme));
499 
500             ShapeDrawable foreground = (ShapeDrawable) mShape.getDrawable(1);
501 
502             foreground.setIntrinsicHeight(background.getIntrinsicHeight() - borderWidth);
503             foreground.setIntrinsicWidth(background.getIntrinsicWidth() - borderWidth);
504             TypedArray ta = view.getContext().obtainStyledAttributes(
505                     new int[]{android.R.attr.colorPrimary});
506             int primaryColor = ta.getColor(0, 0);
507             ta.recycle();
508             int foregroundColor =
509                     ResourceUtils.getColorAttr(view.getContext(), android.R.attr.textColorPrimary);
510 
511             foreground.setTint(ColorUtils.blendARGB(primaryColor, foregroundColor, .05f));
512 
513             thumb.setImageDrawable(mShape);
514             view.setContentDescription(mLabel);
515         }
516 
517         @Override
isActive(CustomizationManager<ThemeComponentOption> manager)518         public boolean isActive(CustomizationManager<ThemeComponentOption> manager) {
519             CustomThemeManager customThemeManager = (CustomThemeManager) manager;
520             return Objects.equals(getOverlayPackages().get(OVERLAY_CATEGORY_SHAPE),
521                     customThemeManager.getOverlayPackages().get(OVERLAY_CATEGORY_SHAPE));
522         }
523 
524         @Override
getLayoutResId()525         public int getLayoutResId() {
526             return R.layout.theme_shape_option;
527         }
528 
529         @Override
bindPreview(ViewGroup container)530         public void bindPreview(ViewGroup container) {
531             container.setContentDescription(
532                     container.getContext().getString(R.string.shape_preview_content_description));
533 
534             bindPreviewHeader(container, R.string.preview_name_shape, R.drawable.ic_shapes_24px,
535                     null);
536 
537             ViewGroup cardBody = container.findViewById(R.id.theme_preview_card_body_container);
538             if (cardBody.getChildCount() == 0) {
539                 LayoutInflater.from(container.getContext()).inflate(
540                         R.layout.preview_card_shape_content, cardBody, true);
541             }
542             for (int i = 0; i < mShapeIconIds.length && i < mAppIcons.size(); i++) {
543                 ImageView iconView = cardBody.findViewById(mShapeIconIds[i]);
544                 iconView.setBackground(mAppIcons.get(i).getDrawableCopy());
545             }
546         }
547 
548         @Override
buildStep(Builder builder)549         public Builder buildStep(Builder builder) {
550             builder.setShapePath(mPath)
551                     .setBottomSheetCornerRadius(mCornerRadius)
552                     .setShapePreviewIcons(mAppIcons);
553             return super.buildStep(builder);
554         }
555     }
556 }
557