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