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