1 /* 2 * Copyright (C) 2017 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 android.app.Activity; 19 import android.app.WallpaperManager; 20 import android.app.wallpaper.WallpaperDescription; 21 import android.content.Context; 22 import android.content.Intent; 23 import android.content.pm.ApplicationInfo; 24 import android.content.pm.PackageManager; 25 import android.content.pm.ResolveInfo; 26 import android.content.res.Resources; 27 import android.net.Uri; 28 import android.os.Parcel; 29 import android.service.wallpaper.WallpaperService; 30 import android.text.TextUtils; 31 import android.util.AttributeSet; 32 import android.util.Log; 33 34 import androidx.annotation.NonNull; 35 import androidx.annotation.Nullable; 36 37 import com.android.wallpaper.R; 38 import com.android.wallpaper.asset.Asset; 39 import com.android.wallpaper.asset.LiveWallpaperThumbAsset; 40 import com.android.wallpaper.module.InjectorProvider; 41 import com.android.wallpaper.module.LiveWallpaperInfoFactory; 42 import com.android.wallpaper.util.ActivityUtils; 43 44 import org.xmlpull.v1.XmlPullParserException; 45 46 import java.io.IOException; 47 import java.text.Collator; 48 import java.util.ArrayList; 49 import java.util.Arrays; 50 import java.util.Collections; 51 import java.util.Comparator; 52 import java.util.Iterator; 53 import java.util.List; 54 import java.util.Set; 55 56 /** 57 * Represents a live wallpaper from the system. 58 */ 59 public class LiveWallpaperInfo extends WallpaperInfo { 60 public static final Creator<LiveWallpaperInfo> CREATOR = 61 new Creator<LiveWallpaperInfo>() { 62 @Override 63 public LiveWallpaperInfo createFromParcel(Parcel in) { 64 return new LiveWallpaperInfo(in); 65 } 66 67 @Override 68 public LiveWallpaperInfo[] newArray(int size) { 69 return new LiveWallpaperInfo[size]; 70 } 71 }; 72 73 public static final String TAG_NAME = "live-wallpaper"; 74 75 private static final String TAG = "LiveWallpaperInfo"; 76 public static final String ATTR_ID = "id"; 77 public static final String ATTR_PACKAGE = "package"; 78 public static final String ATTR_SERVICE = "service"; 79 80 /** 81 * Creates a new {@link LiveWallpaperInfo} from an XML {@link AttributeSet} 82 * @param context used to construct the {@link android.app.WallpaperInfo} associated with the 83 * new {@link LiveWallpaperInfo} 84 * @param categoryId Id of the category the new wallpaper will belong to 85 * @param attrs {@link AttributeSet} to parse 86 * @return a newly created {@link LiveWallpaperInfo} or {@code null} if one couldn't be created. 87 */ 88 @Nullable fromAttributeSet(Context context, String categoryId, AttributeSet attrs)89 public static LiveWallpaperInfo fromAttributeSet(Context context, String categoryId, 90 AttributeSet attrs) { 91 String wallpaperId = attrs.getAttributeValue(null, ATTR_ID); 92 if (TextUtils.isEmpty(wallpaperId)) { 93 Log.w(TAG, "Live wallpaper declaration without id in category " + categoryId); 94 return null; 95 } 96 String packageName = attrs.getAttributeValue(null, ATTR_PACKAGE); 97 String serviceName = attrs.getAttributeValue(null, ATTR_SERVICE); 98 return fromPackageAndServiceName(context, categoryId, wallpaperId, packageName, 99 serviceName); 100 } 101 102 /** 103 * Creates a new {@link LiveWallpaperInfo} from its individual components 104 * @return a newly created {@link LiveWallpaperInfo} or {@code null} if one couldn't be created. 105 */ 106 @Nullable fromPackageAndServiceName(Context context, String categoryId, String wallpaperId, String packageName, String serviceName)107 public static LiveWallpaperInfo fromPackageAndServiceName(Context context, String categoryId, 108 String wallpaperId, String packageName, String serviceName) { 109 if (TextUtils.isEmpty(serviceName)) { 110 Log.w(TAG, "Live wallpaper declaration without service: " + wallpaperId); 111 return null; 112 } 113 114 Intent intent = new Intent(WallpaperService.SERVICE_INTERFACE); 115 if (TextUtils.isEmpty(packageName)) { 116 String [] parts = serviceName.split("/"); 117 if (parts != null && parts.length == 2) { 118 packageName = parts[0]; 119 serviceName = parts[1]; 120 } else { 121 Log.w(TAG, "Live wallpaper declaration with invalid service: " + wallpaperId); 122 return null; 123 } 124 } 125 intent.setClassName(packageName, serviceName); 126 List<ResolveInfo> resolveInfos = context.getPackageManager().queryIntentServices(intent, 127 PackageManager.GET_META_DATA); 128 if (resolveInfos.isEmpty()) { 129 Log.w(TAG, "Couldn't find live wallpaper for " + serviceName); 130 return null; 131 } 132 android.app.WallpaperInfo wallpaperInfo; 133 try { 134 wallpaperInfo = new android.app.WallpaperInfo(context, resolveInfos.get(0)); 135 } catch (XmlPullParserException | IOException e) { 136 Log.w(TAG, "Skipping wallpaper " + resolveInfos.get(0).serviceInfo, e); 137 return null; 138 } 139 140 return new LiveWallpaperInfo(wallpaperInfo, false, categoryId); 141 } 142 getInfo()143 public android.app.WallpaperInfo getInfo() { 144 return mInfo; 145 } 146 getThumbAsset()147 public LiveWallpaperThumbAsset getThumbAsset() { 148 return mThumbAsset; 149 } 150 isVisibleTitle()151 public boolean isVisibleTitle() { 152 return mVisibleTitle; 153 } 154 155 @Nullable getCollectionId()156 public String getCollectionId() { 157 return mCollectionId; 158 } 159 160 protected android.app.WallpaperInfo mInfo; 161 protected LiveWallpaperThumbAsset mThumbAsset; 162 protected boolean mVisibleTitle; 163 @Nullable private final String mCollectionId; 164 @NonNull protected WallpaperDescription mWallpaperDescription; 165 166 /** 167 * Constructs a LiveWallpaperInfo wrapping the given system WallpaperInfo object, representing 168 * a particular live wallpaper. 169 * 170 * @param info 171 */ LiveWallpaperInfo(android.app.WallpaperInfo info)172 public LiveWallpaperInfo(android.app.WallpaperInfo info) { 173 this(info, true, null); 174 } 175 176 /** 177 * Constructs a LiveWallpaperInfo wrapping the given system WallpaperInfo object, representing 178 * a particular live wallpaper. 179 */ LiveWallpaperInfo(android.app.WallpaperInfo info, boolean visibleTitle, @Nullable String collectionId)180 public LiveWallpaperInfo(android.app.WallpaperInfo info, boolean visibleTitle, 181 @Nullable String collectionId) { 182 // TODO (b/373890500) Make info @NonNull and remove null info logic below 183 this(info, visibleTitle, collectionId, 184 new WallpaperDescription.Builder().setComponent( 185 (info != null) ? info.getComponent() : null).build()); 186 } 187 LiveWallpaperInfo(android.app.WallpaperInfo info, boolean visibleTitle, @Nullable String collectionId, @NonNull WallpaperDescription description)188 public LiveWallpaperInfo(android.app.WallpaperInfo info, boolean visibleTitle, 189 @Nullable String collectionId, @NonNull WallpaperDescription description) { 190 mInfo = info; 191 mVisibleTitle = visibleTitle; 192 mCollectionId = collectionId; 193 mWallpaperDescription = description; 194 } 195 LiveWallpaperInfo(Parcel in)196 protected LiveWallpaperInfo(Parcel in) { 197 super(in); 198 mInfo = in.readParcelable(android.app.WallpaperInfo.class.getClassLoader()); 199 mVisibleTitle = in.readInt() == 1; 200 mCollectionId = in.readString(); 201 mWallpaperDescription = in.readParcelable(WallpaperDescription.class.getClassLoader(), 202 WallpaperDescription.class); 203 } 204 205 /** 206 * Returns all live wallpapers found on the device, excluding those residing in APKs described by 207 * the package names in excludedPackageNames. 208 */ getAll(Context context, @Nullable Set<String> excludedPackageNames)209 public static List<WallpaperInfo> getAll(Context context, 210 @Nullable Set<String> excludedPackageNames) { 211 List<ResolveInfo> resolveInfos = getAllOnDevice(context); 212 List<WallpaperInfo> wallpaperInfos = new ArrayList<>(); 213 LiveWallpaperInfoFactory factory = 214 InjectorProvider.getInjector().getLiveWallpaperInfoFactory(context); 215 for (int i = 0; i < resolveInfos.size(); i++) { 216 ResolveInfo resolveInfo = resolveInfos.get(i); 217 android.app.WallpaperInfo wallpaperInfo; 218 try { 219 wallpaperInfo = new android.app.WallpaperInfo(context, resolveInfo); 220 } catch (XmlPullParserException | IOException e) { 221 Log.w(TAG, "Skipping wallpaper " + resolveInfo.serviceInfo, e); 222 continue; 223 } 224 225 if (excludedPackageNames != null && excludedPackageNames.contains( 226 wallpaperInfo.getPackageName())) { 227 continue; 228 } 229 230 wallpaperInfos.add(factory.getLiveWallpaperInfo(wallpaperInfo)); 231 } 232 233 return wallpaperInfos; 234 } 235 236 /** 237 * Returns the live wallpapers having the given service names, found within the APK with the 238 * given package name. 239 */ getFromSpecifiedPackage( Context context, String packageName, @Nullable List<String> serviceNames, boolean shouldShowTitle, String collectionId)240 public static List<WallpaperInfo> getFromSpecifiedPackage( 241 Context context, String packageName, @Nullable List<String> serviceNames, 242 boolean shouldShowTitle, String collectionId) { 243 List<ResolveInfo> resolveInfos; 244 if (serviceNames != null) { 245 resolveInfos = getAllContainingServiceNames(context, serviceNames); 246 } else { 247 resolveInfos = getAllOnDevice(context); 248 } 249 List<WallpaperInfo> wallpaperInfos = new ArrayList<>(); 250 LiveWallpaperInfoFactory factory = 251 InjectorProvider.getInjector().getLiveWallpaperInfoFactory(context); 252 253 for (int i = 0; i < resolveInfos.size(); i++) { 254 ResolveInfo resolveInfo = resolveInfos.get(i); 255 if (resolveInfo == null) { 256 Log.e(TAG, "Found a null resolve info"); 257 continue; 258 } 259 260 android.app.WallpaperInfo wallpaperInfo; 261 try { 262 wallpaperInfo = new android.app.WallpaperInfo(context, resolveInfo); 263 } catch (XmlPullParserException e) { 264 Log.w(TAG, "Skipping wallpaper " + resolveInfo.serviceInfo, e); 265 continue; 266 } catch (IOException e) { 267 Log.w(TAG, "Skipping wallpaper " + resolveInfo.serviceInfo, e); 268 continue; 269 } 270 271 if (!packageName.equals(wallpaperInfo.getPackageName())) { 272 continue; 273 } 274 275 wallpaperInfos.add( 276 factory.getLiveWallpaperInfo(wallpaperInfo, shouldShowTitle, collectionId)); 277 } 278 279 return wallpaperInfos; 280 } 281 282 /** 283 * Returns ResolveInfo objects for all live wallpaper services with the specified fully qualified 284 * service names, keeping order intact. 285 */ getAllContainingServiceNames(Context context, List<String> serviceNames)286 private static List<ResolveInfo> getAllContainingServiceNames(Context context, 287 List<String> serviceNames) { 288 final PackageManager pm = context.getPackageManager(); 289 290 List<ResolveInfo> allResolveInfos = pm.queryIntentServices( 291 new Intent(WallpaperService.SERVICE_INTERFACE), 292 PackageManager.GET_META_DATA); 293 294 // Filter ALL live wallpapers for only those in the list of specified service names. 295 // Prefer this approach so we can make only one call to PackageManager (expensive!) rather than 296 // one call per live wallpaper. 297 ResolveInfo[] specifiedResolveInfos = new ResolveInfo[serviceNames.size()]; 298 for (ResolveInfo resolveInfo : allResolveInfos) { 299 int index = serviceNames.indexOf(resolveInfo.serviceInfo.name); 300 if (index != -1) { 301 specifiedResolveInfos[index] = resolveInfo; 302 } 303 } 304 305 return Arrays.asList(specifiedResolveInfos); 306 } 307 308 /** 309 * Returns ResolveInfo objects for all live wallpaper services installed on the device. System 310 * wallpapers are listed first, unsorted, with other installed wallpapers following sorted 311 * in alphabetical order. 312 */ getAllOnDevice(Context context)313 private static List<ResolveInfo> getAllOnDevice(Context context) { 314 final PackageManager pm = context.getPackageManager(); 315 final String packageName = context.getPackageName(); 316 317 List<ResolveInfo> resolveInfos = pm.queryIntentServices( 318 new Intent(WallpaperService.SERVICE_INTERFACE), 319 PackageManager.GET_META_DATA); 320 321 List<ResolveInfo> wallpaperInfos = new ArrayList<>(); 322 323 // Remove the "Rotating Image Wallpaper" live wallpaper, which is owned by this package, 324 // and separate system wallpapers to sort only non-system ones. 325 Iterator<ResolveInfo> iter = resolveInfos.iterator(); 326 while (iter.hasNext()) { 327 ResolveInfo resolveInfo = iter.next(); 328 if (packageName.equals(resolveInfo.serviceInfo.packageName)) { 329 iter.remove(); 330 } else if (isSystemApp(resolveInfo.serviceInfo.applicationInfo)) { 331 wallpaperInfos.add(resolveInfo); 332 iter.remove(); 333 } 334 } 335 336 if (resolveInfos.isEmpty()) { 337 return wallpaperInfos; 338 } 339 340 // Sort non-system wallpapers alphabetically and append them to system ones 341 Collections.sort(resolveInfos, new Comparator<ResolveInfo>() { 342 final Collator mCollator = Collator.getInstance(); 343 344 @Override 345 public int compare(ResolveInfo info1, ResolveInfo info2) { 346 return mCollator.compare(info1.loadLabel(pm), info2.loadLabel(pm)); 347 } 348 }); 349 wallpaperInfos.addAll(resolveInfos); 350 351 return wallpaperInfos; 352 } 353 isSystemAppWallpaper()354 public boolean isSystemAppWallpaper() { 355 return isSystemApp(mInfo.getServiceInfo().applicationInfo); 356 } 357 /** 358 * @return whether the given app is a system app 359 */ isSystemApp(ApplicationInfo appInfo)360 public static boolean isSystemApp(ApplicationInfo appInfo) { 361 return (appInfo.flags & (ApplicationInfo.FLAG_SYSTEM 362 | ApplicationInfo.FLAG_UPDATED_SYSTEM_APP)) != 0; 363 } 364 setVisibleTitle(boolean visibleTitle)365 public void setVisibleTitle(boolean visibleTitle) { 366 mVisibleTitle = visibleTitle; 367 } 368 369 @Override getTitle(Context context)370 public String getTitle(Context context) { 371 if (mVisibleTitle) { 372 CharSequence labelCharSeq = mInfo.loadLabel(context.getPackageManager()); 373 return labelCharSeq == null ? null : labelCharSeq.toString(); 374 } 375 return null; 376 } 377 378 @Override getAttributions(Context context)379 public List<String> getAttributions(Context context) { 380 List<String> attributions = new ArrayList<>(); 381 PackageManager packageManager = context.getPackageManager(); 382 CharSequence labelCharSeq = mInfo.loadLabel(packageManager); 383 attributions.add(labelCharSeq == null ? null : labelCharSeq.toString()); 384 385 try { 386 CharSequence authorCharSeq = mInfo.loadAuthor(packageManager); 387 if (authorCharSeq != null) { 388 String author = authorCharSeq.toString(); 389 attributions.add(author); 390 } 391 } catch (Resources.NotFoundException e) { 392 // No author specified, so no other attribution to add. 393 } 394 395 try { 396 CharSequence descCharSeq = mInfo.loadDescription(packageManager); 397 if (descCharSeq != null) { 398 String desc = descCharSeq.toString(); 399 attributions.add(desc); 400 } 401 } catch (Resources.NotFoundException e) { 402 // No description specified, so no other attribution to add. 403 } 404 405 return attributions; 406 } 407 408 @Override getActionUrl(Context context)409 public String getActionUrl(Context context) { 410 try { 411 Uri wallpaperContextUri = mInfo.loadContextUri(context.getPackageManager()); 412 return wallpaperContextUri != null ? wallpaperContextUri.toString() : null; 413 } catch (Resources.NotFoundException e) { 414 return null; 415 } 416 } 417 418 /** 419 * Get an optional description for the action button if provided by this LiveWallpaper. 420 */ 421 @Nullable getActionDescription(Context context)422 public CharSequence getActionDescription(Context context) { 423 try { 424 return mInfo.loadContextDescription(context.getPackageManager()); 425 } catch (Resources.NotFoundException e) { 426 return null; 427 } 428 } 429 430 @Override getAsset(Context context)431 public Asset getAsset(Context context) { 432 return null; 433 } 434 435 @Override getThumbAsset(Context context)436 public Asset getThumbAsset(Context context) { 437 if (mThumbAsset == null) { 438 mThumbAsset = new LiveWallpaperThumbAsset(context, mInfo); 439 } 440 return mThumbAsset; 441 } 442 443 @Override showPreview(Activity srcActivity, InlinePreviewIntentFactory factory, int requestCode, boolean isAssetIdPresent)444 public void showPreview(Activity srcActivity, InlinePreviewIntentFactory factory, 445 int requestCode, boolean isAssetIdPresent) { 446 showPreviewActivity(srcActivity, factory, requestCode, isAssetIdPresent, 447 false); 448 } 449 450 @Override showPreview(Activity srcActivity, InlinePreviewIntentFactory factory, int requestCode, boolean isAssetIdPresent, boolean shouldRefreshCategory)451 public void showPreview(Activity srcActivity, InlinePreviewIntentFactory factory, 452 int requestCode, boolean isAssetIdPresent, 453 boolean shouldRefreshCategory) { 454 showPreviewActivity(srcActivity, factory, requestCode, isAssetIdPresent, 455 shouldRefreshCategory); 456 } 457 showPreviewActivity(Activity srcActivity, InlinePreviewIntentFactory factory, int requestCode, boolean isAssetIdPresent, boolean shouldRefreshCategory)458 private void showPreviewActivity(Activity srcActivity, InlinePreviewIntentFactory factory, 459 int requestCode, boolean isAssetIdPresent, 460 boolean shouldRefreshCategory) { 461 //Only use internal live picker if available, otherwise, default to the Framework one 462 if (factory.shouldUseInternalLivePicker(srcActivity)) { 463 srcActivity.startActivityForResult(factory.newIntent(srcActivity, this, 464 isAssetIdPresent, shouldRefreshCategory), requestCode); 465 } else { 466 Intent preview = new Intent(WallpaperManager.ACTION_CHANGE_LIVE_WALLPAPER); 467 preview.putExtra(WallpaperManager.EXTRA_LIVE_WALLPAPER_COMPONENT, mInfo.getComponent()); 468 ActivityUtils.startActivityForResultSafely(srcActivity, preview, requestCode); 469 } 470 } 471 472 @Override writeToParcel(Parcel parcel, int i)473 public void writeToParcel(Parcel parcel, int i) { 474 super.writeToParcel(parcel, i); 475 parcel.writeParcelable(mInfo, 0 /* flags */); 476 parcel.writeInt(mVisibleTitle ? 1 : 0); 477 parcel.writeString(mCollectionId); 478 parcel.writeParcelable(mWallpaperDescription, 0 /* flags */); 479 } 480 481 @Override getWallpaperComponent()482 public android.app.WallpaperInfo getWallpaperComponent() { 483 return mInfo; 484 } 485 486 @Override getCollectionId(Context context)487 public String getCollectionId(Context context) { 488 return TextUtils.isEmpty(mCollectionId) 489 ? context.getString(R.string.live_wallpaper_collection_id) 490 : mCollectionId; 491 } 492 493 @Override getWallpaperId()494 public String getWallpaperId() { 495 return mInfo.getServiceName(); 496 } 497 498 /** 499 * Returns true if this wallpaper is currently applied to either home or lock screen. 500 */ isApplied(@ullable android.app.WallpaperInfo currentHomeWallpaper, @Nullable android.app.WallpaperInfo currentLockWallpaper)501 public boolean isApplied(@Nullable android.app.WallpaperInfo currentHomeWallpaper, 502 @Nullable android.app.WallpaperInfo currentLockWallpaper) { 503 android.app.WallpaperInfo component = getWallpaperComponent(); 504 if (component == null) { 505 return false; 506 } 507 String serviceName = component.getServiceName(); 508 boolean isAppliedToHome = currentHomeWallpaper != null 509 && TextUtils.equals(currentHomeWallpaper.getServiceName(), serviceName); 510 boolean isAppliedToLock = currentLockWallpaper != null 511 && TextUtils.equals(currentLockWallpaper.getServiceName(), serviceName); 512 return isAppliedToHome || isAppliedToLock; 513 } 514 515 @NonNull getWallpaperDescription()516 public WallpaperDescription getWallpaperDescription() { 517 return mWallpaperDescription; 518 } 519 setWallpaperDescription(@onNull WallpaperDescription description)520 public void setWallpaperDescription(@NonNull WallpaperDescription description) { 521 mWallpaperDescription = description; 522 } 523 524 /** 525 * Saves a wallpaper of type LiveWallpaperInfo at a particular destination. 526 * The default implementation simply returns the current wallpaper, but this can be overridden 527 * as per requirement. 528 * 529 * @param context context of the calling activity 530 * @param destination destination of the wallpaper being saved 531 * @return saved LiveWallpaperInfo object 532 */ saveWallpaper(Context context, int destination)533 public LiveWallpaperInfo saveWallpaper(Context context, int destination) { 534 return this; 535 } 536 } 537