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