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