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