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