• 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.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         header.setCompoundDrawablesWithIntrinsicBounds(0, headerIcon, 0, 0);
167         header.setCompoundDrawableTintList(ColorStateList.valueOf(
168                 header.getCurrentTextColor()));
169     }
170 
171     public static class IconOption extends ThemeComponentOption {
172 
173         public static final int THUMBNAIL_ICON_POSITION = 0;
174         private static int[] mIconIds = {
175                 R.id.preview_icon_0, R.id.preview_icon_1, R.id.preview_icon_2, R.id.preview_icon_3,
176                 R.id.preview_icon_4, R.id.preview_icon_5
177         };
178 
179         private List<Drawable> mIcons = new ArrayList<>();
180         private String mLabel;
181 
182         @Override
bindThumbnailTile(View view)183         public void bindThumbnailTile(View view) {
184             Resources res = view.getContext().getResources();
185             Drawable icon = mIcons.get(THUMBNAIL_ICON_POSITION)
186                     .getConstantState().newDrawable().mutate();
187             icon.setTint(res.getColor(R.color.icon_thumbnail_color, null));
188             ((ImageView) view.findViewById(R.id.option_icon)).setImageDrawable(
189                     icon);
190             view.setContentDescription(mLabel);
191         }
192 
193         @Override
isActive(CustomizationManager<ThemeComponentOption> manager)194         public boolean isActive(CustomizationManager<ThemeComponentOption> manager) {
195             CustomThemeManager customThemeManager = (CustomThemeManager) manager;
196             Map<String, String> themePackages = customThemeManager.getOverlayPackages();
197             if (getOverlayPackages().isEmpty()) {
198                 return themePackages.get(OVERLAY_CATEGORY_ICON_SYSUI) == null &&
199                         themePackages.get(OVERLAY_CATEGORY_ICON_SETTINGS) == null &&
200                         themePackages.get(OVERLAY_CATEGORY_ICON_ANDROID) == null &&
201                         themePackages.get(OVERLAY_CATEGORY_ICON_LAUNCHER) == null &&
202                         themePackages.get(OVERLAY_CATEGORY_ICON_THEMEPICKER) == null;
203             }
204             for (Map.Entry<String, String> overlayEntry : getOverlayPackages().entrySet()) {
205                 if(!Objects.equals(overlayEntry.getValue(),
206                         themePackages.get(overlayEntry.getKey()))) {
207                     return false;
208                 }
209             }
210             return true;
211         }
212 
213         @Override
getLayoutResId()214         public int getLayoutResId() {
215             return R.layout.theme_icon_option;
216         }
217 
218         @Override
bindPreview(ViewGroup container)219         public void bindPreview(ViewGroup container) {
220             bindPreviewHeader(container, R.string.preview_name_icon, R.drawable.ic_wifi_24px);
221 
222             ViewGroup cardBody = container.findViewById(R.id.theme_preview_card_body_container);
223             if (cardBody.getChildCount() == 0) {
224                 LayoutInflater.from(container.getContext()).inflate(
225                         R.layout.preview_card_icon_content, cardBody, true);
226             }
227             for (int i = 0; i < mIconIds.length && i < mIcons.size(); i++) {
228                 ((ImageView) container.findViewById(mIconIds[i])).setImageDrawable(
229                         mIcons.get(i));
230             }
231         }
232 
addIcon(Drawable previewIcon)233         public void addIcon(Drawable previewIcon) {
234             mIcons.add(previewIcon);
235         }
236 
237         /**
238          * @return whether this icon option has overlays and previews for all the required packages
239          */
isValid(Context context)240         public boolean isValid(Context context) {
241             return getOverlayPackages().keySet().size() ==
242                     ResourceConstants.getPackagesToOverlay(context).length;
243         }
244 
setLabel(String label)245         public void setLabel(String label) {
246             mLabel = label;
247         }
248 
249         @Override
buildStep(Builder builder)250         public Builder buildStep(Builder builder) {
251             for (Drawable icon : mIcons) {
252                 builder.addIcon(icon);
253             }
254             return super.buildStep(builder);
255         }
256     }
257 
258     public static class ColorOption extends ThemeComponentOption {
259 
260         /**
261          * Ids of views used to represent quick setting tiles in the color preview screen
262          */
263         private static int[] COLOR_TILE_IDS = {
264                 R.id.preview_color_qs_0_bg, R.id.preview_color_qs_1_bg, R.id.preview_color_qs_2_bg
265         };
266 
267         /**
268          * Ids of the views for the foreground of the icon, mapping to the corresponding index of
269          * the actual icon drawable.
270          */
271         static int[][] COLOR_TILES_ICON_IDS = {
272                 new int[]{ R.id.preview_color_qs_0_icon, 0},
273                 new int[]{ R.id.preview_color_qs_1_icon, 1},
274                 new int[] { R.id.preview_color_qs_2_icon, 3}
275         };
276 
277         /**
278          * Ids of views used to represent control buttons in the color preview screen
279          */
280         private static int[] COLOR_BUTTON_IDS = {
281                 R.id.preview_check_selected, R.id.preview_radio_selected,
282                 R.id.preview_toggle_selected
283         };
284 
285         @ColorInt private int mColorAccentLight;
286         @ColorInt private int mColorAccentDark;
287         /**
288          * Icons shown as example of QuickSettings tiles in the color preview screen.
289          */
290         private List<Drawable> mIcons = new ArrayList<>();
291 
292         /**
293          * Drawable with the currently selected shape to be used as background of the sample
294          * QuickSetting icons in the color preview screen.
295          */
296         private Drawable mShapeDrawable;
297 
298         private String mLabel;
299 
ColorOption(String packageName, String label, @ColorInt int lightColor, @ColorInt int darkColor)300         ColorOption(String packageName, String label, @ColorInt int lightColor,
301                 @ColorInt int darkColor) {
302             addOverlayPackage(OVERLAY_CATEGORY_COLOR, packageName);
303             mLabel = label;
304             mColorAccentLight = lightColor;
305             mColorAccentDark = darkColor;
306         }
307 
308         @Override
bindThumbnailTile(View view)309         public void bindThumbnailTile(View view) {
310             @ColorInt int color = resolveColor(view.getResources());
311             ((ImageView) view.findViewById(R.id.option_tile)).setImageTintList(
312                     ColorStateList.valueOf(color));
313             view.setContentDescription(mLabel);
314         }
315 
316         @ColorInt
resolveColor(Resources res)317         private int resolveColor(Resources res) {
318             Configuration configuration = res.getConfiguration();
319             return (configuration.uiMode & Configuration.UI_MODE_NIGHT_MASK)
320                     == Configuration.UI_MODE_NIGHT_YES ? mColorAccentDark : mColorAccentLight;
321         }
322 
323         @Override
isActive(CustomizationManager<ThemeComponentOption> manager)324         public boolean isActive(CustomizationManager<ThemeComponentOption> manager) {
325             CustomThemeManager customThemeManager = (CustomThemeManager) manager;
326             return Objects.equals(getOverlayPackages().get(OVERLAY_CATEGORY_COLOR),
327                     customThemeManager.getOverlayPackages().get(OVERLAY_CATEGORY_COLOR));
328         }
329 
330         @Override
getLayoutResId()331         public int getLayoutResId() {
332             return R.layout.theme_color_option;
333         }
334 
335         @Override
bindPreview(ViewGroup container)336         public void bindPreview(ViewGroup container) {
337             bindPreviewHeader(container, R.string.preview_name_color, R.drawable.ic_colorize_24px);
338 
339             ViewGroup cardBody = container.findViewById(R.id.theme_preview_card_body_container);
340             if (cardBody.getChildCount() == 0) {
341                 LayoutInflater.from(container.getContext()).inflate(
342                         R.layout.preview_card_color_content, cardBody, true);
343             }
344             Resources res = container.getResources();
345             @ColorInt int accentColor = resolveColor(res);
346             @ColorInt int controlGreyColor = res.getColor(R.color.control_grey);
347             ColorStateList tintList = new ColorStateList(
348                     new int[][]{
349                             new int[]{android.R.attr.state_selected},
350                             new int[]{android.R.attr.state_checked},
351                             new int[]{-android.R.attr.state_enabled}
352                     },
353                     new int[] {
354                             accentColor,
355                             accentColor,
356                             controlGreyColor
357                     }
358             );
359 
360             for (int i = 0; i < COLOR_BUTTON_IDS.length; i++) {
361                 CompoundButton button = container.findViewById(COLOR_BUTTON_IDS[i]);
362                 button.setButtonTintList(tintList);
363             }
364 
365             Switch enabledSwitch = container.findViewById(R.id.preview_toggle_selected);
366             enabledSwitch.setThumbTintList(tintList);
367             enabledSwitch.setTrackTintList(tintList);
368 
369             ColorStateList seekbarTintList = ColorStateList.valueOf(accentColor);
370             SeekBar seekbar = container.findViewById(R.id.preview_seekbar);
371             seekbar.setThumbTintList(seekbarTintList);
372             seekbar.setProgressTintList(seekbarTintList);
373             seekbar.setProgressBackgroundTintList(seekbarTintList);
374             // Disable seekbar
375             seekbar.setOnTouchListener((view, motionEvent) -> true);
376             if (!mIcons.isEmpty() && mShapeDrawable != null) {
377                 for (int i = 0; i < COLOR_TILE_IDS.length; i++) {
378                     Drawable icon = mIcons.get(COLOR_TILES_ICON_IDS[i][1]).getConstantState()
379                             .newDrawable();
380                     //TODO: load and set the shape.
381                     Drawable bgShape = mShapeDrawable.getConstantState().newDrawable();
382                     bgShape.setTint(accentColor);
383 
384                     ImageView bg = container.findViewById(COLOR_TILE_IDS[i]);
385                     bg.setImageDrawable(bgShape);
386                     ImageView fg = container.findViewById(COLOR_TILES_ICON_IDS[i][0]);
387                     fg.setImageDrawable(icon);
388                 }
389             }
390         }
391 
setPreviewIcons(List<Drawable> icons)392         public void setPreviewIcons(List<Drawable> icons) {
393             mIcons.addAll(icons);
394         }
395 
setShapeDrawable(@ullable Drawable shapeDrawable)396         public void setShapeDrawable(@Nullable Drawable shapeDrawable) {
397             mShapeDrawable = shapeDrawable;
398         }
399 
400         @Override
buildStep(Builder builder)401         public Builder buildStep(Builder builder) {
402             builder.setColorAccentDark(mColorAccentDark).setColorAccentLight(mColorAccentLight);
403             return super.buildStep(builder);
404         }
405     }
406 
407     public static class ShapeOption extends ThemeComponentOption {
408 
409         private final LayerDrawable mShape;
410         private final List<Drawable> mAppIcons;
411         private final String mLabel;
412         private final Path mPath;
413         private final int mCornerRadius;
414         private int[] mShapeIconIds = {
415                 R.id.shape_preview_icon_0, R.id.shape_preview_icon_1, R.id.shape_preview_icon_2,
416                 R.id.shape_preview_icon_3, R.id.shape_preview_icon_4, R.id.shape_preview_icon_5
417         };
418 
ShapeOption(String packageName, String label, Path path, @Dimension int cornerRadius, Drawable shapeDrawable, List<Drawable> appIcons)419         ShapeOption(String packageName, String label, Path path,
420                 @Dimension int cornerRadius, Drawable shapeDrawable,
421                 List<Drawable> appIcons) {
422             addOverlayPackage(OVERLAY_CATEGORY_SHAPE, packageName);
423             mLabel = label;
424             mAppIcons = appIcons;
425             mPath = path;
426             mCornerRadius = cornerRadius;
427             Drawable background = shapeDrawable.getConstantState().newDrawable();
428             Drawable foreground = shapeDrawable.getConstantState().newDrawable();
429             mShape = new LayerDrawable(new Drawable[]{background, foreground});
430             mShape.setLayerGravity(0, Gravity.CENTER);
431             mShape.setLayerGravity(1, Gravity.CENTER);
432         }
433 
434         @Override
bindThumbnailTile(View view)435         public void bindThumbnailTile(View view) {
436             ImageView thumb = view.findViewById(R.id.shape_thumbnail);
437             Resources res = view.getResources();
438             Theme theme = view.getContext().getTheme();
439             int borderWidth = 2 * res.getDimensionPixelSize(R.dimen.option_border_width);
440 
441             Drawable background = mShape.getDrawable(0);
442             background.setTintList(res.getColorStateList(R.color.option_border_color, theme));
443 
444             ShapeDrawable foreground = (ShapeDrawable) mShape.getDrawable(1);
445 
446             foreground.setIntrinsicHeight(background.getIntrinsicHeight() - borderWidth);
447             foreground.setIntrinsicWidth(background.getIntrinsicWidth() - borderWidth);
448             TypedArray ta = view.getContext().obtainStyledAttributes(
449                     new int[]{android.R.attr.colorPrimary});
450             int primaryColor = ta.getColor(0, 0);
451             ta.recycle();
452             int foregroundColor = res.getColor(R.color.shape_option_tile_foreground_color, theme);
453 
454             foreground.setTint(ColorUtils.blendARGB(primaryColor, foregroundColor, .05f));
455 
456             thumb.setImageDrawable(mShape);
457             view.setContentDescription(mLabel);
458         }
459 
460         @Override
isActive(CustomizationManager<ThemeComponentOption> manager)461         public boolean isActive(CustomizationManager<ThemeComponentOption> manager) {
462             CustomThemeManager customThemeManager = (CustomThemeManager) manager;
463             return Objects.equals(getOverlayPackages().get(OVERLAY_CATEGORY_SHAPE),
464                     customThemeManager.getOverlayPackages().get(OVERLAY_CATEGORY_SHAPE));
465         }
466 
467         @Override
getLayoutResId()468         public int getLayoutResId() {
469             return R.layout.theme_shape_option;
470         }
471 
472         @Override
bindPreview(ViewGroup container)473         public void bindPreview(ViewGroup container) {
474             bindPreviewHeader(container, R.string.preview_name_shape, R.drawable.ic_shapes_24px);
475 
476             ViewGroup cardBody = container.findViewById(R.id.theme_preview_card_body_container);
477             if (cardBody.getChildCount() == 0) {
478                 LayoutInflater.from(container.getContext()).inflate(
479                         R.layout.preview_card_shape_content, cardBody, true);
480             }
481             for (int i = 0; i < mShapeIconIds.length && i < mAppIcons.size(); i++) {
482                 ImageView iconView = cardBody.findViewById(mShapeIconIds[i]);
483                 iconView.setBackground(mAppIcons.get(i));
484             }
485         }
486 
487         @Override
buildStep(Builder builder)488         public Builder buildStep(Builder builder) {
489             builder.setShapePath(mPath).setBottomSheetCornerRadius(mCornerRadius);
490             for (Drawable appIcon : mAppIcons) {
491                 builder.addShapePreviewIcon(appIcon);
492             }
493             return super.buildStep(builder);
494         }
495     }
496 }
497