1 /* 2 * Copyright (C) 2022 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.wallpaper.model; 17 18 import static android.app.Flags.liveWallpaperContentHandling; 19 20 import static com.android.wallpaper.model.CreativeCategory.KEY_WALLPAPER_SAVE_CREATIVE_CATEGORY_WALLPAPER; 21 22 import android.annotation.Nullable; 23 import android.app.WallpaperInfo; 24 import android.app.wallpaper.WallpaperDescription; 25 import android.content.ClipData; 26 import android.content.ContentProviderClient; 27 import android.content.ContentValues; 28 import android.content.Context; 29 import android.content.Intent; 30 import android.content.pm.PackageManager; 31 import android.database.Cursor; 32 import android.net.Uri; 33 import android.os.Bundle; 34 import android.os.Parcel; 35 import android.text.TextUtils; 36 import android.util.Log; 37 38 import androidx.activity.result.contract.ActivityResultContract; 39 import androidx.annotation.NonNull; 40 41 import com.android.wallpaper.asset.Asset; 42 import com.android.wallpaper.asset.CreativeWallpaperThumbAsset; 43 import com.android.wallpaper.module.InjectorProvider; 44 45 import java.util.ArrayList; 46 import java.util.List; 47 48 /** 49 * Represents a creative live wallpaper component. 50 */ 51 public class CreativeWallpaperInfo extends LiveWallpaperInfo { 52 53 public static final Creator<CreativeWallpaperInfo> CREATOR = 54 new Creator<CreativeWallpaperInfo>() { 55 @Override 56 public CreativeWallpaperInfo createFromParcel(Parcel in) { 57 return new CreativeWallpaperInfo(in); 58 } 59 60 @Override 61 public CreativeWallpaperInfo[] newArray(int size) { 62 return new CreativeWallpaperInfo[size]; 63 } 64 }; 65 66 private Uri mConfigPreviewUri; 67 private Uri mCleanPreviewUri; 68 private Uri mDeleteUri; 69 private Uri mThumbnailUri; 70 private Uri mShareUri; 71 private String mTitle; 72 private String mAuthor; 73 private String mDescription; 74 private String mContentDescription; 75 private boolean mIsCurrent; 76 private boolean mIsNewCreativeWallpaper; 77 private String mGroupName; 78 79 private static final String TAG = "CreativeWallpaperInfo"; 80 81 private ArrayList<WallpaperAction> mEffectsToggles = new ArrayList<>(); 82 private String mEffectsBottomSheetTitle; 83 private String mEffectsBottomSheetSubtitle; 84 private Uri mClearActionsUri; 85 private Uri mEffectsUri = null; 86 private String mCurrentlyAppliedEffectId = null; 87 CreativeWallpaperInfo(WallpaperInfo info, String title, @Nullable String author, @Nullable String description, String contentDescription, Uri configPreviewUri, Uri cleanPreviewUri, Uri deleteUri, Uri thumbnailUri, Uri shareUri, String groupName, boolean isCurrent, @NonNull WallpaperDescription wallpaperDescription, boolean isNewCreativeWallpaper)88 public CreativeWallpaperInfo(WallpaperInfo info, String title, @Nullable String author, 89 @Nullable String description, String contentDescription, Uri configPreviewUri, 90 Uri cleanPreviewUri, Uri deleteUri, Uri thumbnailUri, Uri shareUri, String groupName, 91 boolean isCurrent, @NonNull WallpaperDescription wallpaperDescription, 92 boolean isNewCreativeWallpaper) { 93 this(info, /* visibleTitle= */ false, info.getPackageName()); 94 mTitle = title; 95 mAuthor = author; 96 mDescription = description; 97 mContentDescription = contentDescription; 98 mConfigPreviewUri = configPreviewUri; 99 mCleanPreviewUri = cleanPreviewUri; 100 mDeleteUri = deleteUri; 101 mThumbnailUri = thumbnailUri; 102 mShareUri = shareUri; 103 mIsCurrent = isCurrent; 104 mGroupName = groupName; 105 mWallpaperDescription = wallpaperDescription; 106 mIsNewCreativeWallpaper = isNewCreativeWallpaper; 107 } 108 CreativeWallpaperInfo(WallpaperInfo info, boolean isCurrent)109 public CreativeWallpaperInfo(WallpaperInfo info, boolean isCurrent) { 110 this(info, false, info.getPackageName()); 111 mIsCurrent = isCurrent; 112 } 113 CreativeWallpaperInfo(WallpaperInfo info, boolean visibleTitle, @Nullable String collectionId)114 public CreativeWallpaperInfo(WallpaperInfo info, boolean visibleTitle, 115 @Nullable String collectionId) { 116 super(info, visibleTitle, collectionId); 117 } 118 CreativeWallpaperInfo(Parcel in)119 protected CreativeWallpaperInfo(Parcel in) { 120 super(in); 121 mTitle = in.readString(); 122 mAuthor = in.readString(); 123 mDescription = in.readString(); 124 mContentDescription = in.readString(); 125 mIsNewCreativeWallpaper = in.readBoolean(); 126 mConfigPreviewUri = in.readParcelable(Uri.class.getClassLoader(), Uri.class); 127 mCleanPreviewUri = in.readParcelable(Uri.class.getClassLoader(), Uri.class); 128 mDeleteUri = in.readParcelable(Uri.class.getClassLoader(), Uri.class); 129 mThumbnailUri = in.readParcelable(Uri.class.getClassLoader(), Uri.class); 130 mShareUri = in.readParcelable(Uri.class.getClassLoader(), Uri.class); 131 mIsCurrent = in.readBoolean(); 132 mGroupName = in.readString(); 133 mCurrentlyAppliedEffectId = in.readString(); 134 mEffectsUri = in.readParcelable(Uri.class.getClassLoader(), Uri.class); 135 mClearActionsUri = in.readParcelable(Uri.class.getClassLoader(), Uri.class); 136 mEffectsToggles = in.readArrayList(WallpaperAction.class.getClassLoader(), 137 WallpaperAction.class); 138 mEffectsBottomSheetTitle = in.readString(); 139 mEffectsBottomSheetSubtitle = in.readString(); 140 } 141 142 @Override writeToParcel(Parcel parcel, int flags)143 public void writeToParcel(Parcel parcel, int flags) { 144 super.writeToParcel(parcel, flags); 145 parcel.writeString(mTitle); 146 parcel.writeString(mAuthor); 147 parcel.writeString(mDescription); 148 parcel.writeString(mContentDescription); 149 parcel.writeBoolean(mIsNewCreativeWallpaper); 150 parcel.writeParcelable(mConfigPreviewUri, flags); 151 parcel.writeParcelable(mCleanPreviewUri, flags); 152 parcel.writeParcelable(mDeleteUri, flags); 153 parcel.writeParcelable(mThumbnailUri, flags); 154 parcel.writeParcelable(mShareUri, flags); 155 parcel.writeBoolean(mIsCurrent); 156 parcel.writeString(mGroupName); 157 parcel.writeString(mCurrentlyAppliedEffectId); 158 parcel.writeParcelable(mEffectsUri, flags); 159 parcel.writeParcelable(mClearActionsUri, flags); 160 parcel.writeList(mEffectsToggles); 161 parcel.writeString(mEffectsBottomSheetTitle); 162 parcel.writeString(mEffectsBottomSheetSubtitle); 163 } 164 165 /** 166 * Creates a new {@link ActivityResultContract} used to request the settings Activity overlay 167 * for this wallpaper. 168 * 169 * @param intent settings intent 170 */ getContract(Intent intent)171 public static ActivityResultContract<Void, Integer> getContract(Intent intent) { 172 return new ActivityResultContract<Void, Integer>() { 173 @NonNull 174 @Override 175 public Intent createIntent(@NonNull Context context, Void unused) { 176 return intent; 177 } 178 179 @Override 180 public Integer parseResult(int i, @Nullable Intent intent) { 181 return i; 182 } 183 }; 184 } 185 186 /** 187 * Loads the current wallpaper's effects. 188 * 189 * @param context context of the current android component 190 * @return an array list of WallpaperAction data objects 191 * for the currently previewing wallpaper 192 */ 193 @Nullable 194 public ArrayList<WallpaperAction> getWallpaperEffects(Context context) { 195 if (mEffectsUri == null) { 196 return null; 197 } 198 mEffectsToggles.clear(); 199 // TODO (269350033): Move content provider query off the main thread. 200 try (ContentProviderClient effectsClient = 201 context.getContentResolver().acquireContentProviderClient( 202 mEffectsUri.getAuthority())) { 203 try (Cursor effectsCursor = effectsClient.query(mEffectsUri, /* projection= */ null, 204 /* selection= */ null, /* selectionArgs= */ null, /* sortOrder= */ null)) { 205 if (effectsCursor == null) { 206 return null; 207 } 208 while (effectsCursor.moveToNext()) { 209 Uri effectsToggleUri = Uri.parse( 210 effectsCursor.getString(effectsCursor.getColumnIndex( 211 WallpaperInfoContract.WALLPAPER_EFFECTS_TOGGLE_URI))); 212 String effectsButtonLabel = effectsCursor.getString( 213 effectsCursor.getColumnIndex( 214 WallpaperInfoContract.WALLPAPER_EFFECTS_BUTTON_LABEL)); 215 String effectsId = effectsCursor.getString( 216 effectsCursor.getColumnIndex( 217 WallpaperInfoContract.WALLPAPER_EFFECTS_TOGGLE_ID)); 218 mEffectsToggles.add(new WallpaperAction(effectsButtonLabel, 219 effectsToggleUri, effectsId, /* toggled= */ false)); 220 } 221 return mEffectsToggles; 222 } 223 } catch (Exception e) { 224 Log.e(TAG, "Read wallpaper effects with exception.", e); 225 } 226 return null; 227 } 228 229 @Override 230 public String getTitle(Context context) { 231 if (mVisibleTitle) { 232 return mTitle; 233 } 234 return null; 235 } 236 237 @Override 238 public List<String> getAttributions(Context context) { 239 PackageManager packageManager = context.getPackageManager(); 240 CharSequence labelCharSeq = mInfo.loadLabel(packageManager); 241 List<String> attributions = new ArrayList<>(); 242 attributions.add(labelCharSeq == null ? null : labelCharSeq.toString()); 243 attributions.add(mAuthor == null ? "" : mAuthor); 244 attributions.add(mDescription == null ? "" : mDescription); 245 246 return attributions; 247 } 248 249 @Override 250 public String getContentDescription(Context context) { 251 return mContentDescription; 252 } 253 254 @Override 255 public Asset getThumbAsset(Context context) { 256 if (mThumbAsset == null) { 257 mThumbAsset = new CreativeWallpaperThumbAsset(context, mInfo, mThumbnailUri); 258 } 259 return mThumbAsset; 260 } 261 262 @Override 263 public String getGroupName(Context context) { 264 return mGroupName; 265 } 266 267 /** 268 * Calls the config URI to initialize the preview for this wallpaper. 269 */ 270 public void initializeWallpaperPreview(Context context) { 271 if (mConfigPreviewUri != null) { 272 context.getContentResolver().update(mConfigPreviewUri, new ContentValues(), null); 273 } 274 } 275 276 /** 277 * Calls the clean URI to de-initialize the preview for this wallpaper. 278 */ 279 public void cleanUpWallpaperPreview(Context context) { 280 if (mCleanPreviewUri != null) { 281 context.getContentResolver().update(mCleanPreviewUri, new ContentValues(), null); 282 } 283 } 284 285 /** 286 * Returns true if this wallpaper can be deleted. 287 */ 288 public boolean canBeDeleted() { 289 return mDeleteUri != null && !TextUtils.isEmpty(mDeleteUri.toString()); 290 } 291 292 public boolean getIsNewCreativeWallpaper() { 293 return mIsNewCreativeWallpaper; 294 } 295 296 @Override 297 public boolean isApplied(@Nullable WallpaperInfo currentHomeWallpaper, 298 @Nullable WallpaperInfo currentLockWallpaper) { 299 return super.isApplied(currentHomeWallpaper, currentLockWallpaper) && mIsCurrent; 300 } 301 302 /** 303 * Requests the content provider to delete this wallpaper. 304 */ 305 public void requestDelete(Context context) { 306 context.getContentResolver().delete(mDeleteUri, null, null); 307 } 308 309 public void setEffectsToggles(ArrayList<WallpaperAction> effectsToggles) { 310 mEffectsToggles = effectsToggles; 311 } 312 313 public ArrayList<WallpaperAction> getEffectsToggles() { 314 return mEffectsToggles; 315 } 316 317 public String getEffectsBottomSheetTitle() { 318 return mEffectsBottomSheetTitle; 319 } 320 321 public void setEffectsBottomSheetTitle(String effectsBottomSheetTitle) { 322 mEffectsBottomSheetTitle = effectsBottomSheetTitle; 323 } 324 325 /** 326 * Returns the URI that can be used to save a creative category wallpaper. 327 * @return the save wallpaper URI 328 */ 329 public Uri getSaveWallpaperUriForCreativeWallpaper() { 330 Bundle metaData = this.getWallpaperComponent().getServiceInfo().metaData; 331 if (metaData == null || !metaData.containsKey( 332 KEY_WALLPAPER_SAVE_CREATIVE_CATEGORY_WALLPAPER)) { 333 return null; 334 } 335 String keyForCreativeCategoryWallpaper = (String) metaData.get( 336 KEY_WALLPAPER_SAVE_CREATIVE_CATEGORY_WALLPAPER); 337 return Uri.parse(keyForCreativeCategoryWallpaper); 338 } 339 340 public String getEffectsBottomSheetSubtitle() { 341 return mEffectsBottomSheetSubtitle; 342 } 343 344 public void setEffectsBottomSheetSubtitle(String effectsBottomSheetSubtitle) { 345 mEffectsBottomSheetSubtitle = effectsBottomSheetSubtitle; 346 } 347 348 public Uri getClearActionsUri() { 349 return mClearActionsUri; 350 } 351 352 public void setClearActionsUri(Uri clearActionsUri) { 353 mClearActionsUri = clearActionsUri; 354 } 355 356 public Uri getEffectsUri() { 357 return mEffectsUri; 358 } 359 360 public void setEffectsUri(Uri effectsUri) { 361 mEffectsUri = effectsUri; 362 } 363 364 public String getCurrentlyAppliedEffectId() { 365 return mCurrentlyAppliedEffectId; 366 } 367 368 public void setCurrentlyAppliedEffectId(String currentlyAppliedEffectId) { 369 mCurrentlyAppliedEffectId = currentlyAppliedEffectId; 370 } 371 /** 372 * Returns true if this wallpaper can be shared. 373 */ 374 public boolean canBeShared() { 375 return mShareUri != null && !TextUtils.isEmpty(mShareUri.toString()); 376 } 377 378 /** 379 * Gets the share wallpaper image intent. 380 */ 381 public Intent getShareIntent() { 382 Intent shareIntent = new Intent(Intent.ACTION_SEND); 383 shareIntent.putExtra(Intent.EXTRA_STREAM, mShareUri); 384 shareIntent.setType("image/*"); 385 shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 386 shareIntent.setClipData(ClipData.newRawUri(null, mShareUri)); 387 return Intent.createChooser(shareIntent, null); 388 } 389 390 /** 391 * This method returns whether wallpaper effects are supported for this wallpaper. 392 * @return boolean 393 */ 394 public boolean doesSupportWallpaperEffects() { 395 return (mClearActionsUri != null && !TextUtils.isEmpty(mEffectsBottomSheetTitle)); 396 } 397 398 /** 399 * Triggers the content provider call to clear all effects upon the current. 400 * wallpaper. 401 */ 402 public void clearEffects(Context context) { 403 if (doesSupportWallpaperEffects() && mClearActionsUri != null) { 404 context.getContentResolver().update(mClearActionsUri, new ContentValues(), null); 405 } 406 } 407 408 /** 409 * Triggers the content provider call to apply the selected effect upon the current 410 * wallpaper. 411 */ 412 public void applyEffect(Context context, Uri applyEffectUri) { 413 if (doesSupportWallpaperEffects()) { 414 context.getContentResolver().update(applyEffectUri, new ContentValues(), null); 415 } 416 } 417 418 /** 419 * Creates an object of CreativeWallpaperInfo from the given cursor object. 420 * 421 * @param wallpaperInfo contains relevant metadata information about creative-category wallpaper 422 * @param cursor contains relevant info to create an object of CreativeWallpaperInfo 423 * @return an object of type CreativeWallpaperInfo 424 */ 425 @NonNull 426 public static CreativeWallpaperInfo buildFromCursor(WallpaperInfo wallpaperInfo, 427 Cursor cursor) { 428 String wallpaperTitle = cursor.getString( 429 cursor.getColumnIndex(WallpaperInfoContract.WALLPAPER_TITLE)); 430 String wallpaperAuthor = null; 431 if (cursor.getColumnIndex(WallpaperInfoContract.WALLPAPER_AUTHOR) >= 0) { 432 wallpaperAuthor = cursor.getString( 433 cursor.getColumnIndex(WallpaperInfoContract.WALLPAPER_AUTHOR)); 434 } 435 String wallpaperDescription = null; 436 if (cursor.getColumnIndex(WallpaperInfoContract.WALLPAPER_DESCRIPTION) >= 0) { 437 wallpaperDescription = cursor.getString( 438 cursor.getColumnIndex(WallpaperInfoContract.WALLPAPER_DESCRIPTION)); 439 } 440 String wallpaperContentDescription = null; 441 int wallpaperContentDescriptionIndex = cursor.getColumnIndex( 442 WallpaperInfoContract.WALLPAPER_CONTENT_DESCRIPTION); 443 if (wallpaperContentDescriptionIndex >= 0) { 444 wallpaperContentDescription = cursor.getString( 445 wallpaperContentDescriptionIndex); 446 } 447 Uri thumbnailUri = Uri.parse(cursor.getString(cursor.getColumnIndex( 448 WallpaperInfoContract.WALLPAPER_THUMBNAIL))); 449 Uri configPreviewUri = Uri.parse(cursor.getString(cursor.getColumnIndex( 450 WallpaperInfoContract.WALLPAPER_CONFIG_PREVIEW_URI))); 451 Uri cleanPreviewUri = Uri.parse(cursor.getString(cursor.getColumnIndex( 452 WallpaperInfoContract.WALLPAPER_CLEAN_PREVIEW_URI))); 453 Uri deleteUri = Uri.parse(cursor.getString( 454 cursor.getColumnIndex(WallpaperInfoContract.WALLPAPER_DELETE_URI))); 455 Uri shareUri = Uri.parse(cursor.getString(cursor.getColumnIndex( 456 WallpaperInfoContract.WALLPAPER_SHARE_URI))); 457 String groupName = cursor.getString( 458 cursor.getColumnIndex(WallpaperInfoContract.WALLPAPER_GROUP_NAME)); 459 int isCurrentApplied = cursor.getInt( 460 cursor.getColumnIndex(WallpaperInfoContract.WALLPAPER_IS_APPLIED)); 461 WallpaperDescription descriptionContentHandling = 462 new WallpaperDescription.Builder().setComponent( 463 wallpaperInfo.getComponent()).build(); 464 if (liveWallpaperContentHandling()) { 465 int descriptionContentHandlingIndex = cursor.getColumnIndex( 466 WallpaperInfoContract.WALLPAPER_DESCRIPTION_CONTENT_HANDLING); 467 if (descriptionContentHandlingIndex >= 0) { 468 descriptionContentHandling = descriptionFromBytes( 469 cursor.getBlob(descriptionContentHandlingIndex)); 470 if (descriptionContentHandling.getComponent() == null) { 471 descriptionContentHandling = 472 descriptionContentHandling.toBuilder().setComponent( 473 wallpaperInfo.getComponent()).build(); 474 } 475 } 476 } 477 Boolean isNewCreativeWallpaper; 478 if (InjectorProvider.getInjector().getFlags().isNewCreativeWallpaperCategoryEnabled()) { 479 int isNewCreativeWallpaperIndex = cursor.getColumnIndex( 480 WallpaperInfoContract.WALLPAPER_IS_NEW_CREATIVE_WALLPAPER); 481 if (isNewCreativeWallpaperIndex >= 0) { 482 isNewCreativeWallpaper = cursor.getInt(isNewCreativeWallpaperIndex) > 0; 483 } else { 484 Boolean canDelete = deleteUri != null && !TextUtils.isEmpty(deleteUri.toString()); 485 isNewCreativeWallpaper = !canDelete; 486 } 487 } else { 488 isNewCreativeWallpaper = false; 489 } 490 491 return new CreativeWallpaperInfo(wallpaperInfo, wallpaperTitle, wallpaperAuthor, 492 wallpaperDescription, wallpaperContentDescription, configPreviewUri, 493 cleanPreviewUri, deleteUri, thumbnailUri, shareUri, groupName, /* isCurrent= */ 494 (isCurrentApplied == 1), descriptionContentHandling, isNewCreativeWallpaper); 495 } 496 497 private static WallpaperDescription descriptionFromBytes(byte[] bytes) { 498 Parcel parcel = Parcel.obtain(); 499 parcel.unmarshall(bytes, 0, bytes.length); 500 parcel.setDataPosition(0); 501 WallpaperDescription desc = WallpaperDescription.CREATOR.createFromParcel(parcel); 502 parcel.recycle(); 503 return desc; 504 } 505 506 /** 507 * Saves a wallpaper of type of CreativeWallpaperInfo for a particular destination. 508 * @param context context of the calling activity 509 * @param destination depicts the destination of the wallpaper being saved 510 * @return CreativeWallpaperInfo object that has been saved 511 */ 512 @Override 513 public CreativeWallpaperInfo saveWallpaper(Context context, int destination) { 514 if (context == null) { 515 Log.w(TAG, "Context is null!!"); 516 return null; 517 } 518 519 Uri saveWallpaperUri = getSaveWallpaperUriForCreativeWallpaper(); 520 if (saveWallpaperUri == null) { 521 Log.w(TAG, "Missing save wallpaper uri in " + this.getWallpaperComponent() 522 .getServiceName()); 523 return null; 524 } 525 return CreativeCategory.saveCreativeCategoryWallpaper( 526 context, this, saveWallpaperUri, destination); 527 } 528 529 public void setConfigPreviewUri(Uri configPreviewUri) { 530 mConfigPreviewUri = configPreviewUri; 531 } 532 533 public Uri getConfigPreviewUri() { 534 return mConfigPreviewUri; 535 } 536 537 public Uri getCleanPreviewUri() { 538 return mCleanPreviewUri; 539 } 540 541 public Uri getDeleteUri() { 542 return mDeleteUri; 543 } 544 545 public Uri getThumbnailUri() { 546 return mThumbnailUri; 547 } 548 549 public Uri getShareUri() { 550 return mShareUri; 551 } 552 553 public String getTitle() { 554 return mTitle; 555 } 556 557 public String getAuthor() { 558 return mAuthor; 559 } 560 561 public String getDescription() { 562 return mDescription; 563 } 564 565 public String getContentDescription() { 566 return mContentDescription; 567 } 568 569 public boolean isCurrent() { 570 return mIsCurrent; 571 } 572 573 public String getGroupName() { 574 return mGroupName; 575 } 576 } 577