/* * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.wallpaper.model; import android.app.Activity; import android.app.WallpaperManager; import android.content.Context; import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.res.Resources; import android.net.Uri; import android.os.Parcel; import android.service.wallpaper.WallpaperService; import android.text.TextUtils; import android.util.AttributeSet; import android.util.Log; import androidx.annotation.Nullable; import com.android.wallpaper.R; import com.android.wallpaper.asset.Asset; import com.android.wallpaper.asset.LiveWallpaperThumbAsset; import com.android.wallpaper.module.InjectorProvider; import com.android.wallpaper.module.LiveWallpaperInfoFactory; import com.android.wallpaper.util.ActivityUtils; import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; import java.text.Collator; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.Iterator; import java.util.List; import java.util.Set; /** * Represents a live wallpaper from the system. */ public class LiveWallpaperInfo extends WallpaperInfo { public static final Creator CREATOR = new Creator() { @Override public LiveWallpaperInfo createFromParcel(Parcel in) { return new LiveWallpaperInfo(in); } @Override public LiveWallpaperInfo[] newArray(int size) { return new LiveWallpaperInfo[size]; } }; public static final String TAG_NAME = "live-wallpaper"; private static final String TAG = "LiveWallpaperInfo"; public static final String ATTR_ID = "id"; public static final String ATTR_PACKAGE = "package"; public static final String ATTR_SERVICE = "service"; /** * Creates a new {@link LiveWallpaperInfo} from an XML {@link AttributeSet} * @param context used to construct the {@link android.app.WallpaperInfo} associated with the * new {@link LiveWallpaperInfo} * @param categoryId Id of the category the new wallpaper will belong to * @param attrs {@link AttributeSet} to parse * @return a newly created {@link LiveWallpaperInfo} or {@code null} if one couldn't be created. */ @Nullable public static LiveWallpaperInfo fromAttributeSet(Context context, String categoryId, AttributeSet attrs) { String wallpaperId = attrs.getAttributeValue(null, ATTR_ID); if (TextUtils.isEmpty(wallpaperId)) { Log.w(TAG, "Live wallpaper declaration without id in category " + categoryId); return null; } String packageName = attrs.getAttributeValue(null, ATTR_PACKAGE); String serviceName = attrs.getAttributeValue(null, ATTR_SERVICE); return fromPackageAndServiceName(context, categoryId, wallpaperId, packageName, serviceName); } /** * Creates a new {@link LiveWallpaperInfo} from its individual components * @return a newly created {@link LiveWallpaperInfo} or {@code null} if one couldn't be created. */ @Nullable public static LiveWallpaperInfo fromPackageAndServiceName(Context context, String categoryId, String wallpaperId, String packageName, String serviceName) { if (TextUtils.isEmpty(serviceName)) { Log.w(TAG, "Live wallpaper declaration without service: " + wallpaperId); return null; } Intent intent = new Intent(WallpaperService.SERVICE_INTERFACE); if (TextUtils.isEmpty(packageName)) { String [] parts = serviceName.split("/"); if (parts != null && parts.length == 2) { packageName = parts[0]; serviceName = parts[1]; } else { Log.w(TAG, "Live wallpaper declaration with invalid service: " + wallpaperId); return null; } } intent.setClassName(packageName, serviceName); List resolveInfos = context.getPackageManager().queryIntentServices(intent, PackageManager.GET_META_DATA); if (resolveInfos.isEmpty()) { Log.w(TAG, "Couldn't find live wallpaper for " + serviceName); return null; } android.app.WallpaperInfo wallpaperInfo; try { wallpaperInfo = new android.app.WallpaperInfo(context, resolveInfos.get(0)); } catch (XmlPullParserException | IOException e) { Log.w(TAG, "Skipping wallpaper " + resolveInfos.get(0).serviceInfo, e); return null; } return new LiveWallpaperInfo(wallpaperInfo, false, categoryId); } public android.app.WallpaperInfo getInfo() { return mInfo; } public LiveWallpaperThumbAsset getThumbAsset() { return mThumbAsset; } public boolean isVisibleTitle() { return mVisibleTitle; } @Nullable public String getCollectionId() { return mCollectionId; } protected android.app.WallpaperInfo mInfo; protected LiveWallpaperThumbAsset mThumbAsset; protected boolean mVisibleTitle; @Nullable private final String mCollectionId; /** * Constructs a LiveWallpaperInfo wrapping the given system WallpaperInfo object, representing * a particular live wallpaper. * * @param info */ public LiveWallpaperInfo(android.app.WallpaperInfo info) { this(info, true, null); } /** * Constructs a LiveWallpaperInfo wrapping the given system WallpaperInfo object, representing * a particular live wallpaper. */ public LiveWallpaperInfo(android.app.WallpaperInfo info, boolean visibleTitle, @Nullable String collectionId) { mInfo = info; mVisibleTitle = visibleTitle; mCollectionId = collectionId; } protected LiveWallpaperInfo(Parcel in) { super(in); mInfo = in.readParcelable(android.app.WallpaperInfo.class.getClassLoader()); mVisibleTitle = in.readInt() == 1; mCollectionId = in.readString(); } /** * Returns all live wallpapers found on the device, excluding those residing in APKs described by * the package names in excludedPackageNames. */ public static List getAll(Context context, @Nullable Set excludedPackageNames) { List resolveInfos = getAllOnDevice(context); List wallpaperInfos = new ArrayList<>(); LiveWallpaperInfoFactory factory = InjectorProvider.getInjector().getLiveWallpaperInfoFactory(context); for (int i = 0; i < resolveInfos.size(); i++) { ResolveInfo resolveInfo = resolveInfos.get(i); android.app.WallpaperInfo wallpaperInfo; try { wallpaperInfo = new android.app.WallpaperInfo(context, resolveInfo); } catch (XmlPullParserException | IOException e) { Log.w(TAG, "Skipping wallpaper " + resolveInfo.serviceInfo, e); continue; } if (excludedPackageNames != null && excludedPackageNames.contains( wallpaperInfo.getPackageName())) { continue; } wallpaperInfos.add(factory.getLiveWallpaperInfo(wallpaperInfo)); } return wallpaperInfos; } /** * Returns the live wallpapers having the given service names, found within the APK with the * given package name. */ public static List getFromSpecifiedPackage( Context context, String packageName, @Nullable List serviceNames, boolean shouldShowTitle, String collectionId) { List resolveInfos; if (serviceNames != null) { resolveInfos = getAllContainingServiceNames(context, serviceNames); } else { resolveInfos = getAllOnDevice(context); } List wallpaperInfos = new ArrayList<>(); LiveWallpaperInfoFactory factory = InjectorProvider.getInjector().getLiveWallpaperInfoFactory(context); for (int i = 0; i < resolveInfos.size(); i++) { ResolveInfo resolveInfo = resolveInfos.get(i); if (resolveInfo == null) { Log.e(TAG, "Found a null resolve info"); continue; } android.app.WallpaperInfo wallpaperInfo; try { wallpaperInfo = new android.app.WallpaperInfo(context, resolveInfo); } catch (XmlPullParserException e) { Log.w(TAG, "Skipping wallpaper " + resolveInfo.serviceInfo, e); continue; } catch (IOException e) { Log.w(TAG, "Skipping wallpaper " + resolveInfo.serviceInfo, e); continue; } if (!packageName.equals(wallpaperInfo.getPackageName())) { continue; } wallpaperInfos.add( factory.getLiveWallpaperInfo(wallpaperInfo, shouldShowTitle, collectionId)); } return wallpaperInfos; } /** * Returns ResolveInfo objects for all live wallpaper services with the specified fully qualified * service names, keeping order intact. */ private static List getAllContainingServiceNames(Context context, List serviceNames) { final PackageManager pm = context.getPackageManager(); List allResolveInfos = pm.queryIntentServices( new Intent(WallpaperService.SERVICE_INTERFACE), PackageManager.GET_META_DATA); // Filter ALL live wallpapers for only those in the list of specified service names. // Prefer this approach so we can make only one call to PackageManager (expensive!) rather than // one call per live wallpaper. ResolveInfo[] specifiedResolveInfos = new ResolveInfo[serviceNames.size()]; for (ResolveInfo resolveInfo : allResolveInfos) { int index = serviceNames.indexOf(resolveInfo.serviceInfo.name); if (index != -1) { specifiedResolveInfos[index] = resolveInfo; } } return Arrays.asList(specifiedResolveInfos); } /** * Returns ResolveInfo objects for all live wallpaper services installed on the device. System * wallpapers are listed first, unsorted, with other installed wallpapers following sorted * in alphabetical order. */ private static List getAllOnDevice(Context context) { final PackageManager pm = context.getPackageManager(); final String packageName = context.getPackageName(); List resolveInfos = pm.queryIntentServices( new Intent(WallpaperService.SERVICE_INTERFACE), PackageManager.GET_META_DATA); List wallpaperInfos = new ArrayList<>(); // Remove the "Rotating Image Wallpaper" live wallpaper, which is owned by this package, // and separate system wallpapers to sort only non-system ones. Iterator iter = resolveInfos.iterator(); while (iter.hasNext()) { ResolveInfo resolveInfo = iter.next(); if (packageName.equals(resolveInfo.serviceInfo.packageName)) { iter.remove(); } else if (isSystemApp(resolveInfo.serviceInfo.applicationInfo)) { wallpaperInfos.add(resolveInfo); iter.remove(); } } if (resolveInfos.isEmpty()) { return wallpaperInfos; } // Sort non-system wallpapers alphabetically and append them to system ones Collections.sort(resolveInfos, new Comparator() { final Collator mCollator = Collator.getInstance(); @Override public int compare(ResolveInfo info1, ResolveInfo info2) { return mCollator.compare(info1.loadLabel(pm), info2.loadLabel(pm)); } }); wallpaperInfos.addAll(resolveInfos); return wallpaperInfos; } /** * @return whether the given app is a system app */ public static boolean isSystemApp(ApplicationInfo appInfo) { return (appInfo.flags & (ApplicationInfo.FLAG_SYSTEM | ApplicationInfo.FLAG_UPDATED_SYSTEM_APP)) != 0; } public void setVisibleTitle(boolean visibleTitle) { mVisibleTitle = visibleTitle; } @Override public String getTitle(Context context) { if (mVisibleTitle) { CharSequence labelCharSeq = mInfo.loadLabel(context.getPackageManager()); return labelCharSeq == null ? null : labelCharSeq.toString(); } return null; } @Override public List getAttributions(Context context) { List attributions = new ArrayList<>(); PackageManager packageManager = context.getPackageManager(); CharSequence labelCharSeq = mInfo.loadLabel(packageManager); attributions.add(labelCharSeq == null ? null : labelCharSeq.toString()); try { CharSequence authorCharSeq = mInfo.loadAuthor(packageManager); if (authorCharSeq != null) { String author = authorCharSeq.toString(); attributions.add(author); } } catch (Resources.NotFoundException e) { // No author specified, so no other attribution to add. } try { CharSequence descCharSeq = mInfo.loadDescription(packageManager); if (descCharSeq != null) { String desc = descCharSeq.toString(); attributions.add(desc); } } catch (Resources.NotFoundException e) { // No description specified, so no other attribution to add. } return attributions; } @Override public String getActionUrl(Context context) { try { Uri wallpaperContextUri = mInfo.loadContextUri(context.getPackageManager()); return wallpaperContextUri != null ? wallpaperContextUri.toString() : null; } catch (Resources.NotFoundException e) { return null; } } /** * Get an optional description for the action button if provided by this LiveWallpaper. */ @Nullable public CharSequence getActionDescription(Context context) { try { return mInfo.loadContextDescription(context.getPackageManager()); } catch (Resources.NotFoundException e) { return null; } } @Override public Asset getAsset(Context context) { return null; } @Override public Asset getThumbAsset(Context context) { if (mThumbAsset == null) { mThumbAsset = new LiveWallpaperThumbAsset(context, mInfo); } return mThumbAsset; } @Override public void showPreview(Activity srcActivity, InlinePreviewIntentFactory factory, int requestCode, boolean isAssetIdPresent) { //Only use internal live picker if available, otherwise, default to the Framework one if (factory.shouldUseInternalLivePicker(srcActivity)) { srcActivity.startActivityForResult(factory.newIntent(srcActivity, this, isAssetIdPresent), requestCode); } else { Intent preview = new Intent(WallpaperManager.ACTION_CHANGE_LIVE_WALLPAPER); preview.putExtra(WallpaperManager.EXTRA_LIVE_WALLPAPER_COMPONENT, mInfo.getComponent()); ActivityUtils.startActivityForResultSafely(srcActivity, preview, requestCode); } } @Override public void writeToParcel(Parcel parcel, int i) { super.writeToParcel(parcel, i); parcel.writeParcelable(mInfo, 0 /* flags */); parcel.writeInt(mVisibleTitle ? 1 : 0); parcel.writeString(mCollectionId); } @Override public android.app.WallpaperInfo getWallpaperComponent() { return mInfo; } @Override public String getCollectionId(Context context) { return TextUtils.isEmpty(mCollectionId) ? context.getString(R.string.live_wallpaper_collection_id) : mCollectionId; } @Override public String getWallpaperId() { return mInfo.getServiceName(); } /** * Returns true if this wallpaper is currently applied to either home or lock screen. */ public boolean isApplied(@Nullable android.app.WallpaperInfo currentHomeWallpaper, @Nullable android.app.WallpaperInfo currentLockWallpaper) { android.app.WallpaperInfo component = getWallpaperComponent(); if (component == null) { return false; } String serviceName = component.getServiceName(); boolean isAppliedToHome = currentHomeWallpaper != null && TextUtils.equals(currentHomeWallpaper.getServiceName(), serviceName); boolean isAppliedToLock = currentLockWallpaper != null && TextUtils.equals(currentLockWallpaper.getServiceName(), serviceName); return isAppliedToHome || isAppliedToLock; } /** * Saves a wallpaper of type LiveWallpaperInfo at a particular destination. * The default implementation simply returns the current wallpaper, but this can be overridden * as per requirement. * * @param context context of the calling activity * @param destination destination of the wallpaper being saved * @return saved LiveWallpaperInfo object */ public LiveWallpaperInfo saveWallpaper(Context context, int destination) { return this; } }