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; 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_SHAPE; 22 import static com.android.customization.model.ResourceConstants.PATH_SIZE; 23 24 import android.content.Context; 25 import android.content.pm.PackageManager; 26 import android.content.res.Configuration; 27 import android.content.res.Resources; 28 import android.graphics.Path; 29 import android.graphics.Typeface; 30 import android.graphics.drawable.AdaptiveIconDrawable; 31 import android.graphics.drawable.Drawable; 32 import android.graphics.drawable.ShapeDrawable; 33 import android.graphics.drawable.shapes.PathShape; 34 import android.icu.text.SimpleDateFormat; 35 import android.text.TextUtils; 36 import android.util.Log; 37 import android.view.View; 38 import android.widget.ImageView; 39 import android.widget.TextView; 40 41 import androidx.annotation.ColorInt; 42 import androidx.annotation.Dimension; 43 import androidx.annotation.DrawableRes; 44 import androidx.annotation.Nullable; 45 import androidx.annotation.StringRes; 46 import androidx.core.graphics.PathParser; 47 48 import com.android.customization.model.CustomizationManager; 49 import com.android.customization.model.CustomizationOption; 50 import com.android.customization.widget.DynamicAdaptiveIconDrawable; 51 import com.android.wallpaper.R; 52 import com.android.wallpaper.asset.Asset; 53 import com.android.wallpaper.asset.BitmapCachingAsset; 54 import com.android.wallpaper.model.LiveWallpaperInfo; 55 import com.android.wallpaper.model.WallpaperInfo; 56 57 import org.json.JSONException; 58 import org.json.JSONObject; 59 60 import java.util.ArrayList; 61 import java.util.Collections; 62 import java.util.Date; 63 import java.util.HashMap; 64 import java.util.HashSet; 65 import java.util.Iterator; 66 import java.util.List; 67 import java.util.Locale; 68 import java.util.Map; 69 import java.util.Set; 70 71 /** 72 * Represents a Theme component available in the system as a "persona" bundle. 73 * Note that in this context a Theme is not related to Android's Styles, but it's rather an 74 * abstraction representing a series of overlays to be applied to the system. 75 */ 76 public class ThemeBundle implements CustomizationOption<ThemeBundle> { 77 78 private static final String TAG = "ThemeBundle"; 79 private final static String EMPTY_JSON = "{}"; 80 private final static String TIMESTAMP_FIELD = "_applied_timestamp"; 81 82 private final String mTitle; 83 private final PreviewInfo mPreviewInfo; 84 private final boolean mIsDefault; 85 protected final Map<String, String> mPackagesByCategory; 86 @Nullable private final WallpaperInfo mWallpaperInfo; 87 @Nullable private final String mWallpaperOptions; 88 private WallpaperInfo mOverrideWallpaper; 89 private Asset mOverrideWallpaperAsset; 90 private CharSequence mContentDescription; 91 ThemeBundle(String title, Map<String, String> overlayPackages, boolean isDefault, @Nullable WallpaperInfo wallpaperInfo, @Nullable String wallpaperOptions, PreviewInfo previewInfo)92 protected ThemeBundle(String title, Map<String, String> overlayPackages, 93 boolean isDefault, @Nullable WallpaperInfo wallpaperInfo, 94 @Nullable String wallpaperOptions, PreviewInfo previewInfo) { 95 mTitle = title; 96 mIsDefault = isDefault; 97 mPreviewInfo = previewInfo; 98 mWallpaperInfo = wallpaperInfo; 99 mWallpaperOptions = wallpaperOptions; 100 mPackagesByCategory = Collections.unmodifiableMap(overlayPackages); 101 } 102 103 @Override getTitle()104 public String getTitle() { 105 return mTitle; 106 } 107 108 @Override bindThumbnailTile(View view)109 public void bindThumbnailTile(View view) { 110 Resources res = view.getContext().getResources(); 111 112 ((TextView) view.findViewById(R.id.theme_option_font)).setTypeface( 113 mPreviewInfo.headlineFontFamily); 114 if (mPreviewInfo.shapeDrawable != null) { 115 ((ShapeDrawable) mPreviewInfo.shapeDrawable).getPaint().setColor( 116 mPreviewInfo.resolveAccentColor(res)); 117 ((ImageView) view.findViewById(R.id.theme_option_shape)).setImageDrawable( 118 mPreviewInfo.shapeDrawable); 119 } 120 if (!mPreviewInfo.icons.isEmpty()) { 121 Drawable icon = mPreviewInfo.icons.get(0).getConstantState().newDrawable().mutate(); 122 icon.setTint(res.getColor(R.color.icon_thumbnail_color, null)); 123 ((ImageView) view.findViewById(R.id.theme_option_icon)).setImageDrawable( 124 icon); 125 } 126 view.setContentDescription(getContentDescription(view.getContext())); 127 } 128 129 @Override isActive(CustomizationManager<ThemeBundle> manager)130 public boolean isActive(CustomizationManager<ThemeBundle> manager) { 131 ThemeManager themeManager = (ThemeManager) manager; 132 133 if (mIsDefault) { 134 String serializedOverlays = themeManager.getStoredOverlays(); 135 return TextUtils.isEmpty(serializedOverlays) || EMPTY_JSON.equals(serializedOverlays); 136 } else { 137 Map<String, String> currentOverlays = themeManager.getCurrentOverlays(); 138 return mPackagesByCategory.equals(currentOverlays); 139 } 140 } 141 142 @Override getLayoutResId()143 public int getLayoutResId() { 144 return R.layout.theme_option; 145 } 146 147 /** 148 * This is similar to #equals() but it only compares this theme's packages with the other, that 149 * is, it will return true if applying this theme has the same effect of applying the given one. 150 */ isEquivalent(ThemeBundle other)151 public boolean isEquivalent(ThemeBundle other) { 152 if (other == null) { 153 return false; 154 } 155 if (mIsDefault) { 156 return other.isDefault() || TextUtils.isEmpty(other.getSerializedPackages()) 157 || EMPTY_JSON.equals(other.getSerializedPackages()); 158 } 159 // Map#equals ensures keys and values are compared. 160 return mPackagesByCategory.equals(other.mPackagesByCategory); 161 } 162 getPreviewInfo()163 public PreviewInfo getPreviewInfo() { 164 return mPreviewInfo; 165 } 166 setOverrideThemeWallpaper(WallpaperInfo homeWallpaper)167 public void setOverrideThemeWallpaper(WallpaperInfo homeWallpaper) { 168 mOverrideWallpaper = homeWallpaper; 169 mOverrideWallpaperAsset = null; 170 } 171 shouldUseThemeWallpaper()172 public boolean shouldUseThemeWallpaper() { 173 return mOverrideWallpaper == null && mWallpaperInfo != null; 174 } 175 getWallpaperPreviewAsset(Context context)176 public Asset getWallpaperPreviewAsset(Context context) { 177 return mOverrideWallpaper != null ? 178 getOverrideWallpaperAsset(context) : 179 getPreviewInfo().wallpaperAsset; 180 } 181 getOverrideWallpaperAsset(Context context)182 private Asset getOverrideWallpaperAsset(Context context) { 183 if (mOverrideWallpaperAsset == null) { 184 mOverrideWallpaperAsset = new BitmapCachingAsset(context, 185 mOverrideWallpaper.getThumbAsset(context)); 186 } 187 return mOverrideWallpaperAsset; 188 } 189 getWallpaperInfo()190 public WallpaperInfo getWallpaperInfo() { 191 return mWallpaperInfo; 192 } 193 194 @Nullable getWallpaperOptions()195 public String getWallpaperOptions() { 196 return mWallpaperOptions; 197 } 198 isDefault()199 boolean isDefault() { 200 return mIsDefault; 201 } 202 getPackagesByCategory()203 public Map<String, String> getPackagesByCategory() { 204 return mPackagesByCategory; 205 } 206 getSerializedPackages()207 public String getSerializedPackages() { 208 return getJsonPackages(false).toString(); 209 } 210 getSerializedPackagesWithTimestamp()211 public String getSerializedPackagesWithTimestamp() { 212 return getJsonPackages(true).toString(); 213 } 214 getJsonPackages(boolean insertTimestamp)215 JSONObject getJsonPackages(boolean insertTimestamp) { 216 if (isDefault()) { 217 return new JSONObject(); 218 } 219 JSONObject json = new JSONObject(mPackagesByCategory); 220 // Remove items with null values to avoid deserialization issues. 221 removeNullValues(json); 222 if (insertTimestamp) { 223 try { 224 json.put(TIMESTAMP_FIELD, System.currentTimeMillis()); 225 } catch (JSONException e) { 226 Log.e(TAG, "Couldn't add timestamp to serialized themebundle"); 227 } 228 } 229 return json; 230 } 231 removeNullValues(JSONObject json)232 private void removeNullValues(JSONObject json) { 233 Iterator<String> keys = json.keys(); 234 Set<String> keysToRemove = new HashSet<>(); 235 while(keys.hasNext()) { 236 String key = keys.next(); 237 if (json.isNull(key)) { 238 keysToRemove.add(key); 239 } 240 } 241 for (String key : keysToRemove) { 242 json.remove(key); 243 } 244 } 245 getContentDescription(Context context)246 protected CharSequence getContentDescription(Context context) { 247 if (mContentDescription == null) { 248 CharSequence defaultName = context.getString(R.string.default_theme_title); 249 if (isDefault()) { 250 mContentDescription = defaultName; 251 } else { 252 PackageManager pm = context.getPackageManager(); 253 CharSequence fontName = getOverlayName(pm, OVERLAY_CATEGORY_FONT); 254 CharSequence iconName = getOverlayName(pm, OVERLAY_CATEGORY_ICON_ANDROID); 255 CharSequence shapeName = getOverlayName(pm, OVERLAY_CATEGORY_SHAPE); 256 CharSequence colorName = getOverlayName(pm, OVERLAY_CATEGORY_COLOR); 257 mContentDescription = context.getString(R.string.theme_description, 258 TextUtils.isEmpty(fontName) ? defaultName : fontName, 259 TextUtils.isEmpty(iconName) ? defaultName : iconName, 260 TextUtils.isEmpty(shapeName) ? defaultName : shapeName, 261 TextUtils.isEmpty(colorName) ? defaultName : colorName); 262 } 263 } 264 return mContentDescription; 265 } 266 getOverlayName(PackageManager pm, String overlayCategoryFont)267 private CharSequence getOverlayName(PackageManager pm, String overlayCategoryFont) { 268 try { 269 return pm.getApplicationInfo( 270 mPackagesByCategory.get(overlayCategoryFont), 0).loadLabel(pm); 271 } catch (PackageManager.NameNotFoundException e) { 272 return ""; 273 } 274 } 275 276 public static class PreviewInfo { 277 public final Typeface bodyFontFamily; 278 public final Typeface headlineFontFamily; 279 @ColorInt public final int colorAccentLight; 280 @ColorInt public final int colorAccentDark; 281 public final List<Drawable> icons; 282 public final Drawable shapeDrawable; 283 @Nullable public final Asset wallpaperAsset; 284 public final List<Drawable> shapeAppIcons; 285 @Dimension public final int bottomSheeetCornerRadius; 286 PreviewInfo(Context context, Typeface bodyFontFamily, Typeface headlineFontFamily, int colorAccentLight, int colorAccentDark, List<Drawable> icons, Drawable shapeDrawable, @Dimension int cornerRadius, @Nullable Asset wallpaperAsset, List<Drawable> shapeAppIcons)287 private PreviewInfo(Context context, Typeface bodyFontFamily, Typeface headlineFontFamily, 288 int colorAccentLight, int colorAccentDark, List<Drawable> icons, 289 Drawable shapeDrawable, @Dimension int cornerRadius, 290 @Nullable Asset wallpaperAsset, List<Drawable> shapeAppIcons) { 291 this.bodyFontFamily = bodyFontFamily; 292 this.headlineFontFamily = headlineFontFamily; 293 this.colorAccentLight = colorAccentLight; 294 this.colorAccentDark = colorAccentDark; 295 this.icons = icons; 296 this.shapeDrawable = shapeDrawable; 297 this.bottomSheeetCornerRadius = cornerRadius; 298 this.wallpaperAsset = wallpaperAsset == null 299 ? null : new BitmapCachingAsset(context, wallpaperAsset); 300 this.shapeAppIcons = shapeAppIcons; 301 } 302 303 /** 304 * Returns the accent color to be applied corresponding with the current configuration's 305 * UI mode. 306 * @return one of {@link #colorAccentDark} or {@link #colorAccentLight} 307 */ 308 @ColorInt resolveAccentColor(Resources res)309 public int resolveAccentColor(Resources res) { 310 return (res.getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) 311 == Configuration.UI_MODE_NIGHT_YES ? colorAccentDark : colorAccentLight; 312 } 313 } 314 315 public static class Builder { 316 protected String mTitle; 317 private Typeface mBodyFontFamily; 318 private Typeface mHeadlineFontFamily; 319 @ColorInt private int mColorAccentLight = -1; 320 @ColorInt private int mColorAccentDark = -1; 321 private List<Drawable> mIcons = new ArrayList<>(); 322 private String mPathString; 323 private Path mShapePath; 324 private boolean mIsDefault; 325 @Dimension private int mCornerRadius; 326 private Asset mWallpaperAsset; 327 private WallpaperInfo mWallpaperInfo; 328 private String mWallpaperOptions; 329 protected Map<String, String> mPackages = new HashMap<>(); 330 private List<Drawable> mAppIcons = new ArrayList<>(); 331 build(Context context)332 public ThemeBundle build(Context context) { 333 return new ThemeBundle(mTitle, mPackages, mIsDefault, mWallpaperInfo, mWallpaperOptions, 334 createPreviewInfo(context)); 335 } 336 createPreviewInfo(Context context)337 public PreviewInfo createPreviewInfo(Context context) { 338 ShapeDrawable shapeDrawable = null; 339 List<Drawable> shapeIcons = new ArrayList<>(); 340 Path path = mShapePath; 341 if (!TextUtils.isEmpty(mPathString)) { 342 path = PathParser.createPathFromPathData(mPathString); 343 } 344 if (path != null) { 345 PathShape shape = new PathShape(path, PATH_SIZE, PATH_SIZE); 346 shapeDrawable = new ShapeDrawable(shape); 347 shapeDrawable.setIntrinsicHeight((int) PATH_SIZE); 348 shapeDrawable.setIntrinsicWidth((int) PATH_SIZE); 349 for (Drawable icon : mAppIcons) { 350 if (icon instanceof AdaptiveIconDrawable) { 351 AdaptiveIconDrawable adaptiveIcon = (AdaptiveIconDrawable) icon; 352 shapeIcons.add(new DynamicAdaptiveIconDrawable(adaptiveIcon.getBackground(), 353 adaptiveIcon.getForeground(), path)); 354 } else if (icon instanceof DynamicAdaptiveIconDrawable) { 355 shapeIcons.add(icon); 356 } 357 // TODO: add iconloader library's legacy treatment helper methods for 358 // non-adaptive icons 359 } 360 } 361 return new PreviewInfo(context, mBodyFontFamily, mHeadlineFontFamily, mColorAccentLight, 362 mColorAccentDark, mIcons, shapeDrawable, mCornerRadius, 363 mWallpaperAsset, shapeIcons); 364 } 365 getPackages()366 public Map<String, String> getPackages() { 367 return Collections.unmodifiableMap(mPackages); 368 } 369 getTitle()370 public String getTitle() { 371 return mTitle; 372 } 373 setTitle(String title)374 public Builder setTitle(String title) { 375 mTitle = title; 376 return this; 377 } 378 setBodyFontFamily(@ullable Typeface bodyFontFamily)379 public Builder setBodyFontFamily(@Nullable Typeface bodyFontFamily) { 380 mBodyFontFamily = bodyFontFamily; 381 return this; 382 } 383 setHeadlineFontFamily(@ullable Typeface headlineFontFamily)384 public Builder setHeadlineFontFamily(@Nullable Typeface headlineFontFamily) { 385 mHeadlineFontFamily = headlineFontFamily; 386 return this; 387 } 388 setColorAccentLight(@olorInt int colorAccentLight)389 public Builder setColorAccentLight(@ColorInt int colorAccentLight) { 390 mColorAccentLight = colorAccentLight; 391 return this; 392 } 393 setColorAccentDark(@olorInt int colorAccentDark)394 public Builder setColorAccentDark(@ColorInt int colorAccentDark) { 395 mColorAccentDark = colorAccentDark; 396 return this; 397 } 398 addIcon(Drawable icon)399 public Builder addIcon(Drawable icon) { 400 mIcons.add(icon); 401 return this; 402 } 403 addOverlayPackage(String category, String packageName)404 public Builder addOverlayPackage(String category, String packageName) { 405 mPackages.put(category, packageName); 406 return this; 407 } 408 setShapePath(String path)409 public Builder setShapePath(String path) { 410 mPathString = path; 411 return this; 412 } 413 setShapePath(Path path)414 public Builder setShapePath(Path path) { 415 mShapePath = path; 416 return this; 417 } 418 setWallpaperInfo(String wallpaperPackageName, String wallpaperResName, String themeId, @DrawableRes int wallpaperResId, @StringRes int titleResId, @StringRes int attributionResId, @StringRes int actionUrlResId)419 public Builder setWallpaperInfo(String wallpaperPackageName, String wallpaperResName, 420 String themeId, @DrawableRes int wallpaperResId, @StringRes int titleResId, 421 @StringRes int attributionResId, @StringRes int actionUrlResId) { 422 mWallpaperInfo = new ThemeBundledWallpaperInfo(wallpaperPackageName, wallpaperResName, 423 themeId, wallpaperResId, titleResId, attributionResId, actionUrlResId); 424 return this; 425 } 426 setLiveWallpaperInfo(LiveWallpaperInfo info)427 public Builder setLiveWallpaperInfo(LiveWallpaperInfo info) { 428 mWallpaperInfo = info; 429 return this; 430 } 431 432 setWallpaperAsset(Asset wallpaperAsset)433 public Builder setWallpaperAsset(Asset wallpaperAsset) { 434 mWallpaperAsset = wallpaperAsset; 435 return this; 436 } 437 setWallpaperOptions(String wallpaperOptions)438 public Builder setWallpaperOptions(String wallpaperOptions) { 439 mWallpaperOptions = wallpaperOptions; 440 return this; 441 } 442 asDefault()443 public Builder asDefault() { 444 mIsDefault = true; 445 return this; 446 } 447 addShapePreviewIcon(Drawable appIcon)448 public Builder addShapePreviewIcon(Drawable appIcon) { 449 mAppIcons.add(appIcon); 450 return this; 451 } 452 setBottomSheetCornerRadius(@imension int radius)453 public Builder setBottomSheetCornerRadius(@Dimension int radius) { 454 mCornerRadius = radius; 455 return this; 456 } 457 } 458 } 459