| /* |
| * Copyright (C) 2023 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.server.pm; |
| |
| import static android.app.ActivityManager.START_ABORTED; |
| import static android.app.ActivityManager.START_CLASS_NOT_FOUND; |
| import static android.app.ActivityManager.START_PERMISSION_DENIED; |
| import static android.app.ActivityManager.START_SUCCESS; |
| import static android.app.AppOpsManager.MODE_IGNORED; |
| import static android.app.ComponentOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED; |
| import static android.content.pm.ArchivedActivityInfo.bytesFromBitmap; |
| import static android.content.pm.ArchivedActivityInfo.drawableToBitmap; |
| import static android.content.pm.PackageInstaller.EXTRA_UNARCHIVE_STATUS; |
| import static android.content.pm.PackageInstaller.UNARCHIVAL_OK; |
| import static android.content.pm.PackageManager.DELETE_ARCHIVE; |
| import static android.content.pm.PackageManager.DELETE_KEEP_DATA; |
| import static android.content.pm.PackageManager.INSTALL_UNARCHIVE_DRAFT; |
| import static android.os.PowerExemptionManager.REASON_PACKAGE_UNARCHIVE; |
| import static android.os.PowerExemptionManager.TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_ALLOWED; |
| |
| import android.Manifest; |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.annotation.RequiresPermission; |
| import android.annotation.UserIdInt; |
| import android.app.ActivityManager; |
| import android.app.AppOpsManager; |
| import android.app.BroadcastOptions; |
| import android.app.PendingIntent; |
| import android.content.ComponentName; |
| import android.content.Context; |
| import android.content.IIntentReceiver; |
| import android.content.IIntentSender; |
| import android.content.Intent; |
| import android.content.IntentSender; |
| import android.content.pm.ApplicationInfo; |
| import android.content.pm.ArchivedActivityParcel; |
| import android.content.pm.ArchivedPackageParcel; |
| import android.content.pm.LauncherActivityInfo; |
| import android.content.pm.LauncherApps; |
| import android.content.pm.PackageInstaller; |
| import android.content.pm.PackageManager; |
| import android.content.pm.PackageManager.DeleteFlags; |
| import android.content.pm.ParceledListSlice; |
| import android.content.pm.ResolveInfo; |
| import android.content.pm.VersionedPackage; |
| import android.graphics.Bitmap; |
| import android.graphics.BitmapFactory; |
| import android.graphics.Color; |
| import android.graphics.PorterDuff; |
| import android.graphics.PorterDuffColorFilter; |
| import android.graphics.drawable.BitmapDrawable; |
| import android.graphics.drawable.Drawable; |
| import android.graphics.drawable.LayerDrawable; |
| import android.os.Binder; |
| import android.os.Bundle; |
| import android.os.Environment; |
| import android.os.IBinder; |
| import android.os.ParcelableException; |
| import android.os.Process; |
| import android.os.SELinux; |
| import android.os.UserHandle; |
| import android.text.TextUtils; |
| import android.util.ExceptionUtils; |
| import android.util.Slog; |
| |
| import com.android.internal.R; |
| import com.android.internal.annotations.GuardedBy; |
| import com.android.internal.annotations.VisibleForTesting; |
| import com.android.server.pm.pkg.ArchiveState; |
| import com.android.server.pm.pkg.ArchiveState.ArchiveActivityInfo; |
| import com.android.server.pm.pkg.PackageState; |
| import com.android.server.pm.pkg.PackageStateInternal; |
| import com.android.server.pm.pkg.PackageUserState; |
| import com.android.server.pm.pkg.PackageUserStateInternal; |
| |
| import java.io.File; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.nio.file.Path; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Objects; |
| import java.util.concurrent.CompletableFuture; |
| |
| /** |
| * Responsible archiving apps and returning information about archived apps. |
| * |
| * <p> An archived app is in a state where the app is not fully on the device. APKs are removed |
| * while the data directory is kept. Archived apps are included in the list of launcher apps where |
| * tapping them re-installs the full app. |
| */ |
| public class PackageArchiver { |
| |
| private static final String TAG = "PackageArchiverService"; |
| |
| public static final String EXTRA_UNARCHIVE_INTENT_SENDER = |
| "android.content.pm.extra.UNARCHIVE_INTENT_SENDER"; |
| |
| /** |
| * The maximum time granted for an app store to start a foreground service when unarchival |
| * is requested. |
| */ |
| // TODO(b/297358628) Make this configurable through a flag. |
| private static final int DEFAULT_UNARCHIVE_FOREGROUND_TIMEOUT_MS = 120 * 1000; |
| |
| private static final String ARCHIVE_ICONS_DIR = "package_archiver"; |
| |
| private static final String ACTION_UNARCHIVE_DIALOG = |
| "com.android.intent.action.UNARCHIVE_DIALOG"; |
| private static final String ACTION_UNARCHIVE_ERROR_DIALOG = |
| "com.android.intent.action.UNARCHIVE_ERROR_DIALOG"; |
| |
| private static final String EXTRA_REQUIRED_BYTES = |
| "com.android.content.pm.extra.UNARCHIVE_EXTRA_REQUIRED_BYTES"; |
| private static final String EXTRA_INSTALLER_PACKAGE_NAME = |
| "com.android.content.pm.extra.UNARCHIVE_INSTALLER_PACKAGE_NAME"; |
| private static final String EXTRA_INSTALLER_TITLE = |
| "com.android.content.pm.extra.UNARCHIVE_INSTALLER_TITLE"; |
| |
| private final Context mContext; |
| private final PackageManagerService mPm; |
| |
| @Nullable |
| private LauncherApps mLauncherApps; |
| |
| @Nullable |
| private AppOpsManager mAppOpsManager; |
| |
| PackageArchiver(Context context, PackageManagerService mPm) { |
| this.mContext = context; |
| this.mPm = mPm; |
| } |
| |
| /** Returns whether a package is archived for a user. */ |
| public static boolean isArchived(PackageUserState userState) { |
| return userState.getArchiveState() != null && !userState.isInstalled(); |
| } |
| |
| void requestArchive( |
| @NonNull String packageName, |
| @NonNull String callerPackageName, |
| @NonNull IntentSender intentSender, |
| @NonNull UserHandle userHandle, |
| @DeleteFlags int flags) { |
| Objects.requireNonNull(packageName); |
| Objects.requireNonNull(callerPackageName); |
| Objects.requireNonNull(intentSender); |
| Objects.requireNonNull(userHandle); |
| |
| Computer snapshot = mPm.snapshotComputer(); |
| int userId = userHandle.getIdentifier(); |
| int binderUid = Binder.getCallingUid(); |
| if (!PackageManagerServiceUtils.isSystemOrRootOrShell(binderUid)) { |
| verifyCaller(snapshot.getPackageUid(callerPackageName, 0, userId), binderUid); |
| } |
| snapshot.enforceCrossUserPermission(binderUid, userId, true, true, |
| "archiveApp"); |
| verifyUninstallPermissions(); |
| |
| CompletableFuture<ArchiveState> archiveStateFuture; |
| try { |
| archiveStateFuture = createArchiveState(packageName, userId); |
| } catch (PackageManager.NameNotFoundException e) { |
| Slog.d(TAG, TextUtils.formatSimple("Failed to archive %s with message %s", |
| packageName, e.getMessage())); |
| throw new ParcelableException(e); |
| } |
| |
| archiveStateFuture |
| .thenAccept( |
| archiveState -> { |
| // TODO(b/282952870) Should be reverted if uninstall fails/cancels |
| try { |
| storeArchiveState(packageName, archiveState, userId); |
| } catch (PackageManager.NameNotFoundException e) { |
| sendFailureStatus(intentSender, packageName, e.getMessage()); |
| return; |
| } |
| |
| // TODO(b/278553670) Add special strings for the delete dialog |
| mPm.mInstallerService.uninstall( |
| new VersionedPackage(packageName, |
| PackageManager.VERSION_CODE_HIGHEST), |
| callerPackageName, |
| DELETE_ARCHIVE | DELETE_KEEP_DATA | flags, |
| intentSender, |
| userId, |
| binderUid); |
| }) |
| .exceptionally( |
| e -> { |
| sendFailureStatus(intentSender, packageName, e.getMessage()); |
| return null; |
| }); |
| } |
| |
| /** |
| * Starts unarchival for the package corresponding to the startActivity intent. Note that this |
| * will work only if the caller is the default/Home Launcher or if activity is started via Shell |
| * identity. |
| */ |
| @NonNull |
| public int requestUnarchiveOnActivityStart(@Nullable Intent intent, |
| @Nullable String callerPackageName, int userId, int callingUid) { |
| String packageName = getPackageNameFromIntent(intent); |
| if (packageName == null) { |
| Slog.e(TAG, "packageName cannot be null for unarchival!"); |
| return START_CLASS_NOT_FOUND; |
| } |
| if (callerPackageName == null) { |
| Slog.e(TAG, "callerPackageName cannot be null for unarchival!"); |
| return START_CLASS_NOT_FOUND; |
| } |
| if (!isCallingPackageValid(callerPackageName, callingUid, userId)) { |
| // Return early as the calling UID does not match caller package's UID. |
| return START_CLASS_NOT_FOUND; |
| } |
| String currentLauncherPackageName = getCurrentLauncherPackageName(userId); |
| if ((currentLauncherPackageName == null || !callerPackageName.equals( |
| currentLauncherPackageName)) && callingUid != Process.SHELL_UID) { |
| // TODO(b/311619990): Remove dependency on SHELL_UID for testing |
| Slog.e(TAG, TextUtils.formatSimple( |
| "callerPackageName: %s does not qualify for archival of package: " + "%s!", |
| callerPackageName, packageName)); |
| return START_PERMISSION_DENIED; |
| } |
| // TODO(b/302114464): Handle edge cases & also divert to a dialog based on |
| // permissions + compat options |
| Slog.i(TAG, TextUtils.formatSimple("Unarchival is starting for: %s", packageName)); |
| try { |
| final IIntentSender.Stub mLocalSender = new IIntentSender.Stub() { |
| @Override |
| public void send(int code, Intent intent, String resolvedType, |
| IBinder allowlistToken, |
| IIntentReceiver finishedReceiver, String requiredPermission, |
| Bundle options) { |
| // TODO(b/302114464): Handle intent sender status codes |
| } |
| }; |
| |
| requestUnarchive(packageName, callerPackageName, |
| new IntentSender((IIntentSender) mLocalSender), UserHandle.of(userId)); |
| } catch (Throwable t) { |
| Slog.e(TAG, TextUtils.formatSimple( |
| "Unexpected error occurred while unarchiving package %s: %s.", packageName, |
| t.getLocalizedMessage())); |
| return START_ABORTED; |
| } |
| return START_SUCCESS; |
| } |
| |
| /** |
| * Returns true if the componentName targeted by the intent corresponds to that of an archived |
| * app. |
| */ |
| public boolean isIntentResolvedToArchivedApp(Intent intent, int userId) { |
| String packageName = getPackageNameFromIntent(intent); |
| if (packageName == null || intent.getComponent() == null) { |
| return false; |
| } |
| PackageState packageState = mPm.snapshotComputer().getPackageStateInternal(packageName); |
| if (packageState == null) { |
| return false; |
| } |
| PackageUserState userState = packageState.getUserStateOrDefault(userId); |
| if (!PackageArchiver.isArchived(userState)) { |
| return false; |
| } |
| List<ArchiveState.ArchiveActivityInfo> archiveActivityInfoList = |
| userState.getArchiveState().getActivityInfos(); |
| for (int i = 0; i < archiveActivityInfoList.size(); i++) { |
| if (archiveActivityInfoList.get(i) |
| .getOriginalComponentName().equals(intent.getComponent())) { |
| return true; |
| } |
| } |
| Slog.e(TAG, TextUtils.formatSimple( |
| "Package: %s is archived but component to start main activity" |
| + " cannot be found!", packageName)); |
| return false; |
| } |
| |
| @Nullable |
| private String getCurrentLauncherPackageName(int userId) { |
| ComponentName defaultLauncherComponent = mPm.snapshotComputer().getDefaultHomeActivity( |
| userId); |
| if (defaultLauncherComponent != null) { |
| return defaultLauncherComponent.getPackageName(); |
| } |
| return null; |
| } |
| |
| private boolean isCallingPackageValid(String callingPackage, int callingUid, int userId) { |
| int packageUid; |
| packageUid = mPm.snapshotComputer().getPackageUid(callingPackage, 0L, userId); |
| if (packageUid != callingUid) { |
| Slog.w(TAG, TextUtils.formatSimple("Calling package: %s does not belong to uid: %d", |
| callingPackage, callingUid)); |
| return false; |
| } |
| return true; |
| } |
| |
| /** Creates archived state for the package and user. */ |
| private CompletableFuture<ArchiveState> createArchiveState(String packageName, int userId) |
| throws PackageManager.NameNotFoundException { |
| Computer snapshot = mPm.snapshotComputer(); |
| PackageStateInternal ps = getPackageState(packageName, snapshot, |
| Binder.getCallingUid(), userId); |
| verifyNotSystemApp(ps.getFlags()); |
| verifyInstalled(ps, userId); |
| String responsibleInstallerPackage = getResponsibleInstallerPackage(ps); |
| verifyInstaller(responsibleInstallerPackage, userId); |
| ApplicationInfo installerInfo = snapshot.getApplicationInfo( |
| responsibleInstallerPackage, /* flags= */ 0, userId); |
| verifyOptOutStatus(packageName, |
| UserHandle.getUid(userId, UserHandle.getUid(userId, ps.getAppId()))); |
| |
| List<LauncherActivityInfo> mainActivities = getLauncherActivityInfos(ps.getPackageName(), |
| userId); |
| final CompletableFuture<ArchiveState> archiveState = new CompletableFuture<>(); |
| mPm.mHandler.post(() -> { |
| try { |
| archiveState.complete( |
| createArchiveStateInternal(packageName, userId, mainActivities, |
| installerInfo.loadLabel(mContext.getPackageManager()).toString())); |
| } catch (IOException e) { |
| archiveState.completeExceptionally(e); |
| } |
| }); |
| return archiveState; |
| } |
| |
| @Nullable |
| ArchiveState createArchiveState(@NonNull ArchivedPackageParcel archivedPackage, |
| int userId, String installerPackage) { |
| ApplicationInfo installerInfo = mPm.snapshotComputer().getApplicationInfo( |
| installerPackage, /* flags= */ 0, userId); |
| if (installerInfo == null) { |
| // Should never happen because we just fetched the installerInfo. |
| Slog.e(TAG, "Couldnt find installer " + installerPackage); |
| return null; |
| } |
| |
| try { |
| var packageName = archivedPackage.packageName; |
| var mainActivities = archivedPackage.archivedActivities; |
| List<ArchiveActivityInfo> archiveActivityInfos = new ArrayList<>(mainActivities.length); |
| for (int i = 0, size = mainActivities.length; i < size; ++i) { |
| var mainActivity = mainActivities[i]; |
| Path iconPath = storeIconForParcel(packageName, mainActivity, userId, i); |
| ArchiveActivityInfo activityInfo = |
| new ArchiveActivityInfo( |
| mainActivity.title, |
| mainActivity.originalComponentName, |
| iconPath, |
| null); |
| archiveActivityInfos.add(activityInfo); |
| } |
| |
| return new ArchiveState(archiveActivityInfos, |
| installerInfo.loadLabel(mContext.getPackageManager()).toString()); |
| } catch (IOException e) { |
| Slog.e(TAG, "Failed to create archive state", e); |
| return null; |
| } |
| } |
| |
| ArchiveState createArchiveStateInternal(String packageName, int userId, |
| List<LauncherActivityInfo> mainActivities, String installerTitle) |
| throws IOException { |
| final int iconSize = mContext.getSystemService( |
| ActivityManager.class).getLauncherLargeIconSize(); |
| |
| List<ArchiveActivityInfo> archiveActivityInfos = new ArrayList<>(mainActivities.size()); |
| for (int i = 0, size = mainActivities.size(); i < size; i++) { |
| LauncherActivityInfo mainActivity = mainActivities.get(i); |
| Path iconPath = storeIcon(packageName, mainActivity, userId, i, iconSize); |
| ArchiveActivityInfo activityInfo = |
| new ArchiveActivityInfo( |
| mainActivity.getLabel().toString(), |
| mainActivity.getComponentName(), |
| iconPath, |
| null); |
| archiveActivityInfos.add(activityInfo); |
| } |
| |
| return new ArchiveState(archiveActivityInfos, installerTitle); |
| } |
| |
| // TODO(b/298452477) Handle monochrome icons. |
| private static Path storeIconForParcel(String packageName, ArchivedActivityParcel mainActivity, |
| @UserIdInt int userId, int index) throws IOException { |
| if (mainActivity.iconBitmap == null) { |
| return null; |
| } |
| File iconsDir = createIconsDir(userId); |
| File iconFile = new File(iconsDir, packageName + "-" + index + ".png"); |
| try (FileOutputStream out = new FileOutputStream(iconFile)) { |
| out.write(mainActivity.iconBitmap); |
| out.flush(); |
| } |
| return iconFile.toPath(); |
| } |
| |
| @VisibleForTesting |
| Path storeIcon(String packageName, LauncherActivityInfo mainActivity, |
| @UserIdInt int userId, int index, int iconSize) throws IOException { |
| int iconResourceId = mainActivity.getActivityInfo().getIconResource(); |
| if (iconResourceId == 0) { |
| // The app doesn't define an icon. No need to store anything. |
| return null; |
| } |
| File iconsDir = createIconsDir(userId); |
| File iconFile = new File(iconsDir, packageName + "-" + index + ".png"); |
| Bitmap icon = drawableToBitmap(mainActivity.getIcon(/* density= */ 0), iconSize); |
| try (FileOutputStream out = new FileOutputStream(iconFile)) { |
| // Note: Quality is ignored for PNGs. |
| if (!icon.compress(Bitmap.CompressFormat.PNG, /* quality= */ 100, out)) { |
| throw new IOException(TextUtils.formatSimple("Failure to store icon file %s", |
| iconFile.getName())); |
| } |
| out.flush(); |
| } |
| return iconFile.toPath(); |
| } |
| |
| private void verifyInstaller(String installerPackageName, int userId) |
| throws PackageManager.NameNotFoundException { |
| if (TextUtils.isEmpty(installerPackageName)) { |
| throw new PackageManager.NameNotFoundException("No installer found"); |
| } |
| // Allow shell for easier development. |
| if ((Binder.getCallingUid() != Process.SHELL_UID) |
| && !verifySupportsUnarchival(installerPackageName, userId)) { |
| throw new PackageManager.NameNotFoundException("Installer does not support unarchival"); |
| } |
| } |
| |
| /** |
| * Returns true if {@code installerPackage} supports unarchival being able to handle |
| * {@link Intent#ACTION_UNARCHIVE_PACKAGE} |
| */ |
| public boolean verifySupportsUnarchival(String installerPackage, int userId) { |
| if (TextUtils.isEmpty(installerPackage)) { |
| return false; |
| } |
| |
| Intent intent = new Intent(Intent.ACTION_UNARCHIVE_PACKAGE).setPackage(installerPackage); |
| |
| ParceledListSlice<ResolveInfo> intentReceivers = |
| Binder.withCleanCallingIdentity( |
| () -> mPm.queryIntentReceivers(mPm.snapshotComputer(), |
| intent, /* resolvedType= */ null, /* flags= */ 0, userId)); |
| return intentReceivers != null && !intentReceivers.getList().isEmpty(); |
| } |
| |
| private void verifyNotSystemApp(int flags) throws PackageManager.NameNotFoundException { |
| if ((flags & ApplicationInfo.FLAG_SYSTEM) != 0 || ( |
| (flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0)) { |
| throw new PackageManager.NameNotFoundException("System apps cannot be archived."); |
| } |
| } |
| |
| private void verifyInstalled(PackageStateInternal ps, int userId) |
| throws PackageManager.NameNotFoundException { |
| if (!ps.getUserStateOrDefault(userId).isInstalled()) { |
| throw new PackageManager.NameNotFoundException( |
| TextUtils.formatSimple("%s is not installed.", ps.getPackageName())); |
| } |
| } |
| |
| /** |
| * Returns true if the app is archivable. |
| */ |
| // TODO(b/299299569) Exclude system apps |
| public boolean isAppArchivable(@NonNull String packageName, @NonNull UserHandle user) { |
| Objects.requireNonNull(packageName); |
| Objects.requireNonNull(user); |
| |
| Computer snapshot = mPm.snapshotComputer(); |
| int userId = user.getIdentifier(); |
| int binderUid = Binder.getCallingUid(); |
| snapshot.enforceCrossUserPermission(binderUid, userId, true, true, |
| "isAppArchivable"); |
| PackageStateInternal ps; |
| try { |
| ps = getPackageState(packageName, mPm.snapshotComputer(), |
| Binder.getCallingUid(), userId); |
| } catch (PackageManager.NameNotFoundException e) { |
| throw new ParcelableException(e); |
| } |
| |
| if ((ps.getFlags() & ApplicationInfo.FLAG_SYSTEM) != 0 || ( |
| (ps.getFlags() & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0)) { |
| return false; |
| } |
| |
| if (isAppOptedOutOfArchiving(packageName, ps.getAppId())) { |
| return false; |
| } |
| |
| try { |
| verifyInstaller(getResponsibleInstallerPackage(ps), userId); |
| getLauncherActivityInfos(packageName, userId); |
| } catch (PackageManager.NameNotFoundException e) { |
| return false; |
| } |
| |
| return true; |
| } |
| |
| /** |
| * Returns true if user has opted the app out of archiving through system settings. |
| */ |
| private boolean isAppOptedOutOfArchiving(String packageName, int uid) { |
| return Binder.withCleanCallingIdentity(() -> |
| getAppOpsManager().checkOpNoThrow( |
| AppOpsManager.OP_AUTO_REVOKE_PERMISSIONS_IF_UNUSED, uid, packageName) |
| == MODE_IGNORED); |
| } |
| |
| private void verifyOptOutStatus(String packageName, int uid) |
| throws PackageManager.NameNotFoundException { |
| if (isAppOptedOutOfArchiving(packageName, uid)) { |
| throw new PackageManager.NameNotFoundException( |
| TextUtils.formatSimple("The app %s is opted out of archiving.", packageName)); |
| } |
| } |
| |
| void requestUnarchive( |
| @NonNull String packageName, |
| @NonNull String callerPackageName, |
| @NonNull IntentSender statusReceiver, |
| @NonNull UserHandle userHandle) { |
| Objects.requireNonNull(packageName); |
| Objects.requireNonNull(callerPackageName); |
| Objects.requireNonNull(statusReceiver); |
| Objects.requireNonNull(userHandle); |
| |
| Computer snapshot = mPm.snapshotComputer(); |
| int userId = userHandle.getIdentifier(); |
| int binderUid = Binder.getCallingUid(); |
| if (!PackageManagerServiceUtils.isSystemOrRootOrShell(binderUid)) { |
| verifyCaller(snapshot.getPackageUid(callerPackageName, 0, userId), binderUid); |
| } |
| snapshot.enforceCrossUserPermission(binderUid, userId, true, true, |
| "unarchiveApp"); |
| |
| PackageStateInternal ps; |
| PackageStateInternal callerPs; |
| try { |
| ps = getPackageState(packageName, snapshot, binderUid, userId); |
| callerPs = getPackageState(callerPackageName, snapshot, binderUid, userId); |
| verifyArchived(ps, userId); |
| } catch (PackageManager.NameNotFoundException e) { |
| throw new ParcelableException(e); |
| } |
| String installerPackage = getResponsibleInstallerPackage(ps); |
| if (installerPackage == null) { |
| throw new ParcelableException( |
| new PackageManager.NameNotFoundException( |
| TextUtils.formatSimple("No installer found to unarchive app %s.", |
| packageName))); |
| } |
| |
| boolean hasInstallPackages = mContext.checkCallingOrSelfPermission( |
| Manifest.permission.INSTALL_PACKAGES) |
| == PackageManager.PERMISSION_GRANTED; |
| // We don't check the AppOpsManager here for REQUEST_INSTALL_PACKAGES because the requester |
| // is not the source of the installation. |
| boolean hasRequestInstallPackages = callerPs.getAndroidPackage().getRequestedPermissions() |
| .contains(android.Manifest.permission.REQUEST_INSTALL_PACKAGES); |
| if (!hasInstallPackages && !hasRequestInstallPackages) { |
| throw new SecurityException("You need the com.android.permission.INSTALL_PACKAGES " |
| + "or com.android.permission.REQUEST_INSTALL_PACKAGES permission to request " |
| + "an unarchival."); |
| } |
| |
| if (!hasInstallPackages) { |
| requestUnarchiveConfirmation(packageName, statusReceiver); |
| return; |
| } |
| |
| // TODO(b/311709794) Check that the responsible installer has INSTALL_PACKAGES or |
| // OPSTR_REQUEST_INSTALL_PACKAGES too. Edge case: In reality this should always be the case, |
| // unless a user has disabled the permission after archiving an app. |
| |
| int draftSessionId; |
| try { |
| draftSessionId = Binder.withCleanCallingIdentity(() -> |
| createDraftSession(packageName, installerPackage, statusReceiver, userId)); |
| } catch (RuntimeException e) { |
| if (e.getCause() instanceof IOException) { |
| throw ExceptionUtils.wrap((IOException) e.getCause()); |
| } else { |
| throw e; |
| } |
| } |
| |
| mPm.mHandler.post( |
| () -> unarchiveInternal(packageName, userHandle, installerPackage, draftSessionId)); |
| } |
| |
| private void requestUnarchiveConfirmation(String packageName, IntentSender statusReceiver) { |
| final Intent dialogIntent = new Intent(ACTION_UNARCHIVE_DIALOG); |
| dialogIntent.putExtra(EXTRA_UNARCHIVE_INTENT_SENDER, statusReceiver); |
| dialogIntent.putExtra(PackageInstaller.EXTRA_PACKAGE_NAME, packageName); |
| |
| final Intent broadcastIntent = new Intent(); |
| broadcastIntent.putExtra(PackageInstaller.EXTRA_PACKAGE_NAME, packageName); |
| broadcastIntent.putExtra(EXTRA_UNARCHIVE_STATUS, |
| PackageInstaller.STATUS_PENDING_USER_ACTION); |
| broadcastIntent.putExtra(Intent.EXTRA_INTENT, dialogIntent); |
| sendIntent(statusReceiver, packageName, /* message= */ "", broadcastIntent); |
| } |
| |
| private void verifyUninstallPermissions() { |
| if (mContext.checkCallingOrSelfPermission(Manifest.permission.DELETE_PACKAGES) |
| != PackageManager.PERMISSION_GRANTED && mContext.checkCallingOrSelfPermission( |
| Manifest.permission.REQUEST_DELETE_PACKAGES) |
| != PackageManager.PERMISSION_GRANTED) { |
| throw new SecurityException("You need the com.android.permission.DELETE_PACKAGES " |
| + "or com.android.permission.REQUEST_DELETE_PACKAGES permission to request " |
| + "an archival."); |
| } |
| } |
| |
| private int createDraftSession(String packageName, String installerPackage, |
| IntentSender statusReceiver, int userId) throws IOException { |
| PackageInstaller.SessionParams sessionParams = new PackageInstaller.SessionParams( |
| PackageInstaller.SessionParams.MODE_FULL_INSTALL); |
| sessionParams.setAppPackageName(packageName); |
| sessionParams.installFlags = INSTALL_UNARCHIVE_DRAFT; |
| sessionParams.unarchiveIntentSender = statusReceiver; |
| |
| int installerUid = mPm.snapshotComputer().getPackageUid(installerPackage, 0, userId); |
| // Handles case of repeated unarchival calls for the same package. |
| int existingSessionId = mPm.mInstallerService.getExistingDraftSessionId(installerUid, |
| sessionParams, |
| userId); |
| if (existingSessionId != PackageInstaller.SessionInfo.INVALID_ID) { |
| return existingSessionId; |
| } |
| |
| int sessionId = mPm.mInstallerService.createSessionInternal( |
| sessionParams, |
| installerPackage, mContext.getAttributionTag(), |
| installerUid, |
| userId); |
| // TODO(b/297358628) Also cleanup sessions upon device restart. |
| mPm.mHandler.postDelayed(() -> mPm.mInstallerService.cleanupDraftIfUnclaimed(sessionId), |
| getUnarchiveForegroundTimeout()); |
| return sessionId; |
| } |
| |
| /** |
| * Returns the icon of an archived app. This is the icon of the main activity of the app. |
| * |
| * <p> The icon is returned without any treatment/overlay. In the rare case the app had multiple |
| * launcher activities, only one of the icons is returned arbitrarily. |
| */ |
| public Bitmap getArchivedAppIcon(@NonNull String packageName, @NonNull UserHandle user) { |
| Objects.requireNonNull(packageName); |
| Objects.requireNonNull(user); |
| |
| Computer snapshot = mPm.snapshotComputer(); |
| int callingUid = Binder.getCallingUid(); |
| int userId = user.getIdentifier(); |
| PackageStateInternal ps; |
| try { |
| ps = getPackageState(packageName, snapshot, callingUid, userId); |
| } catch (PackageManager.NameNotFoundException e) { |
| Slog.e(TAG, TextUtils.formatSimple("Package %s couldn't be found.", packageName), e); |
| return null; |
| } |
| |
| ArchiveState archiveState = getAnyArchiveState(ps, userId); |
| if (archiveState == null || archiveState.getActivityInfos().size() == 0) { |
| return null; |
| } |
| |
| // TODO(b/298452477) Handle monochrome icons. |
| // In the rare case the archived app defined more than two launcher activities, we choose |
| // the first one arbitrarily. |
| return includeCloudOverlay(decodeIcon(archiveState.getActivityInfos().get(0))); |
| } |
| |
| /** |
| * This method first checks the ArchiveState for the provided userId and then tries to fallback |
| * to other users if the current user is not archived. |
| * |
| * <p> This fallback behaviour is required for archived apps to fit into the multi-user world |
| * where APKs are shared across users. E.g. current ways of fetching icons for apps that are |
| * only installed on the work profile also work when executed on the personal profile if you're |
| * using {@link PackageManager#MATCH_UNINSTALLED_PACKAGES}. Resource fetching from APKs is for |
| * the most part userId-agnostic, which we need to mimic here in order for existing methods |
| * like {@link PackageManager#getApplicationIcon} to continue working. |
| * |
| * @return {@link ArchiveState} for {@code userId} if present. If not present, false back to an |
| * arbitrary userId. If no user is archived, returns null. |
| */ |
| @Nullable |
| private ArchiveState getAnyArchiveState(PackageStateInternal ps, int userId) { |
| PackageUserStateInternal userState = ps.getUserStateOrDefault(userId); |
| if (isArchived(userState)) { |
| return userState.getArchiveState(); |
| } |
| |
| for (int i = 0; i < ps.getUserStates().size(); i++) { |
| userState = ps.getUserStates().valueAt(i); |
| if (isArchived(userState)) { |
| return userState.getArchiveState(); |
| } |
| } |
| |
| return null; |
| } |
| |
| @VisibleForTesting |
| Bitmap decodeIcon(ArchiveActivityInfo archiveActivityInfo) { |
| return BitmapFactory.decodeFile(archiveActivityInfo.getIconBitmap().toString()); |
| } |
| |
| Bitmap includeCloudOverlay(Bitmap bitmap) { |
| Drawable cloudDrawable = |
| mContext.getResources() |
| .getDrawable(R.drawable.archived_app_cloud_overlay, mContext.getTheme()); |
| if (cloudDrawable == null) { |
| Slog.e(TAG, "Unable to locate cloud overlay for archived app!"); |
| return bitmap; |
| } |
| BitmapDrawable appIconDrawable = new BitmapDrawable(mContext.getResources(), bitmap); |
| PorterDuffColorFilter colorFilter = |
| new PorterDuffColorFilter( |
| Color.argb(0.32f /* alpha */, 0f /* red */, 0f /* green */, 0f /* blue */), |
| PorterDuff.Mode.SRC_ATOP); |
| appIconDrawable.setColorFilter(colorFilter); |
| appIconDrawable.setBounds( |
| 0 /* left */, |
| 0 /* top */, |
| cloudDrawable.getIntrinsicWidth(), |
| cloudDrawable.getIntrinsicHeight()); |
| LayerDrawable layerDrawable = |
| new LayerDrawable(new Drawable[]{appIconDrawable, cloudDrawable}); |
| final int iconSize = mContext.getSystemService( |
| ActivityManager.class).getLauncherLargeIconSize(); |
| Bitmap appIconWithCloudOverlay = drawableToBitmap(layerDrawable, iconSize); |
| bitmap.recycle(); |
| return appIconWithCloudOverlay; |
| } |
| |
| private void verifyArchived(PackageStateInternal ps, int userId) |
| throws PackageManager.NameNotFoundException { |
| PackageUserStateInternal userState = ps.getUserStateOrDefault(userId); |
| if (!isArchived(userState)) { |
| throw new PackageManager.NameNotFoundException( |
| TextUtils.formatSimple("Package %s is not currently archived.", |
| ps.getPackageName())); |
| } |
| } |
| |
| @RequiresPermission( |
| allOf = { |
| Manifest.permission.INTERACT_ACROSS_USERS, |
| android.Manifest.permission.CHANGE_DEVICE_IDLE_TEMP_WHITELIST, |
| android.Manifest.permission.START_ACTIVITIES_FROM_BACKGROUND, |
| android.Manifest.permission.START_FOREGROUND_SERVICES_FROM_BACKGROUND}, |
| conditional = true) |
| private void unarchiveInternal(String packageName, UserHandle userHandle, |
| String installerPackage, int unarchiveId) { |
| int userId = userHandle.getIdentifier(); |
| Intent unarchiveIntent = new Intent(Intent.ACTION_UNARCHIVE_PACKAGE); |
| unarchiveIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND); |
| unarchiveIntent.putExtra(PackageInstaller.EXTRA_UNARCHIVE_ID, unarchiveId); |
| unarchiveIntent.putExtra(PackageInstaller.EXTRA_UNARCHIVE_PACKAGE_NAME, packageName); |
| unarchiveIntent.putExtra(PackageInstaller.EXTRA_UNARCHIVE_ALL_USERS, |
| userId == UserHandle.USER_ALL); |
| unarchiveIntent.setPackage(installerPackage); |
| |
| // If the unarchival is requested for all users, the current user is used for unarchival. |
| UserHandle userForUnarchival = userId == UserHandle.USER_ALL |
| ? UserHandle.of(mPm.mUserManager.getCurrentUserId()) |
| : userHandle; |
| mContext.sendOrderedBroadcastAsUser( |
| unarchiveIntent, |
| userForUnarchival, |
| /* receiverPermission = */ null, |
| AppOpsManager.OP_NONE, |
| createUnarchiveOptions(), |
| /* resultReceiver= */ null, |
| /* scheduler= */ null, |
| /* initialCode= */ 0, |
| /* initialData= */ null, |
| /* initialExtras= */ null); |
| } |
| |
| List<LauncherActivityInfo> getLauncherActivityInfos(String packageName, |
| int userId) throws PackageManager.NameNotFoundException { |
| List<LauncherActivityInfo> mainActivities = |
| Binder.withCleanCallingIdentity(() -> getLauncherApps().getActivityList( |
| packageName, |
| new UserHandle(userId))); |
| if (mainActivities.isEmpty()) { |
| throw new PackageManager.NameNotFoundException( |
| TextUtils.formatSimple("The app %s does not have a main activity.", |
| packageName)); |
| } |
| |
| return mainActivities; |
| } |
| |
| @RequiresPermission(anyOf = {android.Manifest.permission.CHANGE_DEVICE_IDLE_TEMP_WHITELIST, |
| android.Manifest.permission.START_ACTIVITIES_FROM_BACKGROUND, |
| android.Manifest.permission.START_FOREGROUND_SERVICES_FROM_BACKGROUND}) |
| private Bundle createUnarchiveOptions() { |
| BroadcastOptions options = BroadcastOptions.makeBasic(); |
| options.setTemporaryAppAllowlist(getUnarchiveForegroundTimeout(), |
| TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_ALLOWED, |
| REASON_PACKAGE_UNARCHIVE, ""); |
| return options.toBundle(); |
| } |
| |
| private static int getUnarchiveForegroundTimeout() { |
| return DEFAULT_UNARCHIVE_FOREGROUND_TIMEOUT_MS; |
| } |
| |
| static String getResponsibleInstallerPackage(PackageStateInternal ps) { |
| return TextUtils.isEmpty(ps.getInstallSource().mUpdateOwnerPackageName) |
| ? ps.getInstallSource().mInstallerPackageName |
| : ps.getInstallSource().mUpdateOwnerPackageName; |
| } |
| |
| void notifyUnarchivalListener(int status, String installerPackageName, String appPackageName, |
| long requiredStorageBytes, @Nullable PendingIntent userActionIntent, |
| IntentSender unarchiveIntentSender, int userId) { |
| final Intent broadcastIntent = new Intent(); |
| broadcastIntent.putExtra(PackageInstaller.EXTRA_PACKAGE_NAME, appPackageName); |
| broadcastIntent.putExtra(EXTRA_UNARCHIVE_STATUS, status); |
| |
| if (status != UNARCHIVAL_OK) { |
| final Intent dialogIntent = createErrorDialogIntent(status, installerPackageName, |
| appPackageName, |
| requiredStorageBytes, userActionIntent, userId); |
| if (dialogIntent == null) { |
| // Error already logged. |
| return; |
| } |
| broadcastIntent.putExtra(Intent.EXTRA_INTENT, dialogIntent); |
| } |
| |
| final BroadcastOptions options = BroadcastOptions.makeBasic(); |
| options.setPendingIntentBackgroundActivityStartMode( |
| MODE_BACKGROUND_ACTIVITY_START_DENIED); |
| try { |
| unarchiveIntentSender.sendIntent(mContext, 0, broadcastIntent, /* onFinished= */ null, |
| /* handler= */ null, /* requiredPermission= */ null, |
| options.toBundle()); |
| } catch (IntentSender.SendIntentException e) { |
| Slog.e(TAG, TextUtils.formatSimple("Failed to send unarchive intent"), e); |
| } |
| } |
| |
| @Nullable |
| private Intent createErrorDialogIntent(int status, String installerPackageName, |
| String appPackageName, |
| long requiredStorageBytes, PendingIntent userActionIntent, int userId) { |
| final Intent dialogIntent = new Intent(ACTION_UNARCHIVE_ERROR_DIALOG); |
| dialogIntent.putExtra(EXTRA_UNARCHIVE_STATUS, status); |
| if (requiredStorageBytes > 0) { |
| dialogIntent.putExtra(EXTRA_REQUIRED_BYTES, requiredStorageBytes); |
| } |
| // Note that the userActionIntent is provided by the installer and is used only by the |
| // system package installer as a follow-up action after the user confirms the dialog. |
| if (userActionIntent != null) { |
| dialogIntent.putExtra(Intent.EXTRA_INTENT, userActionIntent); |
| } |
| dialogIntent.putExtra(EXTRA_INSTALLER_PACKAGE_NAME, installerPackageName); |
| // We fetch this label from the archive state because the installer might not be installed |
| // anymore in an edge case. |
| String installerTitle = getInstallerTitle(appPackageName, userId); |
| if (installerTitle == null) { |
| // Error already logged. |
| return null; |
| } |
| dialogIntent.putExtra(EXTRA_INSTALLER_TITLE, installerTitle); |
| return dialogIntent; |
| } |
| |
| private String getInstallerTitle(String appPackageName, int userId) { |
| PackageStateInternal packageState; |
| try { |
| packageState = getPackageState(appPackageName, |
| mPm.snapshotComputer(), |
| Process.SYSTEM_UID, userId); |
| } catch (PackageManager.NameNotFoundException e) { |
| Slog.e(TAG, TextUtils.formatSimple( |
| "notifyUnarchivalListener: Couldn't fetch package state for %s.", |
| appPackageName), e); |
| return null; |
| } |
| ArchiveState archiveState = packageState.getUserStateOrDefault(userId).getArchiveState(); |
| if (archiveState == null) { |
| Slog.e(TAG, TextUtils.formatSimple("notifyUnarchivalListener: App not archived %s.", |
| appPackageName)); |
| return null; |
| } |
| return archiveState.getInstallerTitle(); |
| } |
| |
| @NonNull |
| private static PackageStateInternal getPackageState(String packageName, |
| Computer snapshot, int callingUid, int userId) |
| throws PackageManager.NameNotFoundException { |
| PackageStateInternal ps = snapshot.getPackageStateFiltered(packageName, callingUid, |
| userId); |
| if (ps == null) { |
| throw new PackageManager.NameNotFoundException( |
| TextUtils.formatSimple("Package %s not found.", packageName)); |
| } |
| return ps; |
| } |
| |
| private LauncherApps getLauncherApps() { |
| if (mLauncherApps == null) { |
| mLauncherApps = mContext.getSystemService(LauncherApps.class); |
| } |
| return mLauncherApps; |
| } |
| |
| private AppOpsManager getAppOpsManager() { |
| if (mAppOpsManager == null) { |
| mAppOpsManager = mContext.getSystemService(AppOpsManager.class); |
| } |
| return mAppOpsManager; |
| } |
| |
| private void storeArchiveState(String packageName, ArchiveState archiveState, int userId) |
| throws PackageManager.NameNotFoundException { |
| synchronized (mPm.mLock) { |
| PackageSetting packageSetting = getPackageSettingLocked(packageName, userId); |
| packageSetting |
| .modifyUserState(userId) |
| .setArchiveState(archiveState); |
| } |
| } |
| |
| @NonNull |
| @GuardedBy("mPm.mLock") |
| private PackageSetting getPackageSettingLocked(String packageName, int userId) |
| throws PackageManager.NameNotFoundException { |
| PackageSetting ps = mPm.mSettings.getPackageLPr(packageName); |
| // Shouldn't happen, we already verify presence of the package in getPackageState() |
| if (ps == null || !ps.getUserStateOrDefault(userId).isInstalled()) { |
| throw new PackageManager.NameNotFoundException( |
| TextUtils.formatSimple("Package %s not found.", packageName)); |
| } |
| return ps; |
| } |
| |
| private void sendFailureStatus(IntentSender statusReceiver, String packageName, |
| String message) { |
| Slog.d(TAG, TextUtils.formatSimple("Failed to archive %s with message %s", packageName, |
| message)); |
| final Intent intent = new Intent(); |
| intent.putExtra(PackageInstaller.EXTRA_PACKAGE_NAME, packageName); |
| intent.putExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE); |
| intent.putExtra(PackageInstaller.EXTRA_STATUS_MESSAGE, message); |
| sendIntent(statusReceiver, packageName, message, intent); |
| } |
| |
| private void sendIntent(IntentSender statusReceiver, String packageName, String message, |
| Intent intent) { |
| try { |
| final BroadcastOptions options = BroadcastOptions.makeBasic(); |
| options.setPendingIntentBackgroundActivityStartMode( |
| MODE_BACKGROUND_ACTIVITY_START_DENIED); |
| statusReceiver.sendIntent(mContext, 0, intent, /* onFinished= */ null, |
| /* handler= */ null, /* requiredPermission= */ null, options.toBundle()); |
| } catch (IntentSender.SendIntentException e) { |
| Slog.e( |
| TAG, |
| TextUtils.formatSimple("Failed to send status for %s with message %s", |
| packageName, message), |
| e); |
| } |
| } |
| |
| private static void verifyCaller(int providedUid, int binderUid) { |
| if (providedUid != binderUid) { |
| throw new SecurityException( |
| TextUtils.formatSimple( |
| "The UID %s of callerPackageName set by the caller doesn't match the " |
| + "caller's actual UID %s.", |
| providedUid, |
| binderUid)); |
| } |
| } |
| |
| private static File createIconsDir(@UserIdInt int userId) throws IOException { |
| File iconsDir = getIconsDir(userId); |
| if (!iconsDir.isDirectory()) { |
| iconsDir.delete(); |
| iconsDir.mkdirs(); |
| if (!iconsDir.isDirectory()) { |
| throw new IOException("Unable to create directory " + iconsDir); |
| } |
| } |
| SELinux.restorecon(iconsDir); |
| return iconsDir; |
| } |
| |
| private static File getIconsDir(int userId) { |
| return new File(Environment.getDataSystemCeDirectory(userId), ARCHIVE_ICONS_DIR); |
| } |
| |
| private static byte[] bytesFromBitmapFile(Path path) throws IOException { |
| if (path == null) { |
| return null; |
| } |
| // Technically we could just read the bytes, but we want to be sure we store the |
| // right format. |
| return bytesFromBitmap(BitmapFactory.decodeFile(path.toString())); |
| } |
| |
| @Nullable |
| private static String getPackageNameFromIntent(@Nullable Intent intent) { |
| if (intent == null) { |
| return null; |
| } |
| if (intent.getPackage() != null) { |
| return intent.getPackage(); |
| } |
| if (intent.getComponent() != null) { |
| return intent.getComponent().getPackageName(); |
| } |
| return null; |
| } |
| |
| /** |
| * Creates serializable archived activities from existing ArchiveState. |
| */ |
| static ArchivedActivityParcel[] createArchivedActivities(ArchiveState archiveState) |
| throws IOException { |
| var infos = archiveState.getActivityInfos(); |
| if (infos == null || infos.isEmpty()) { |
| throw new IllegalArgumentException("No activities in archive state"); |
| } |
| |
| List<ArchivedActivityParcel> activities = new ArrayList<>(infos.size()); |
| for (int i = 0, size = infos.size(); i < size; ++i) { |
| var info = infos.get(i); |
| if (info == null) { |
| continue; |
| } |
| var archivedActivity = new ArchivedActivityParcel(); |
| archivedActivity.title = info.getTitle(); |
| archivedActivity.originalComponentName = info.getOriginalComponentName(); |
| archivedActivity.iconBitmap = bytesFromBitmapFile(info.getIconBitmap()); |
| archivedActivity.monochromeIconBitmap = bytesFromBitmapFile( |
| info.getMonochromeIconBitmap()); |
| activities.add(archivedActivity); |
| } |
| |
| if (activities.isEmpty()) { |
| throw new IllegalArgumentException( |
| "Failed to extract title and icon of main activities"); |
| } |
| |
| return activities.toArray(new ArchivedActivityParcel[activities.size()]); |
| } |
| |
| /** |
| * Creates serializable archived activities from launcher activities. |
| */ |
| static ArchivedActivityParcel[] createArchivedActivities(List<LauncherActivityInfo> infos, |
| int iconSize) throws IOException { |
| if (infos == null || infos.isEmpty()) { |
| throw new IllegalArgumentException("No launcher activities"); |
| } |
| |
| List<ArchivedActivityParcel> activities = new ArrayList<>(infos.size()); |
| for (int i = 0, size = infos.size(); i < size; ++i) { |
| var info = infos.get(i); |
| if (info == null) { |
| continue; |
| } |
| var archivedActivity = new ArchivedActivityParcel(); |
| archivedActivity.title = info.getLabel().toString(); |
| archivedActivity.originalComponentName = info.getComponentName(); |
| archivedActivity.iconBitmap = info.getActivityInfo().getIconResource() == 0 ? null : |
| bytesFromBitmap(drawableToBitmap(info.getIcon(/* density= */ 0), iconSize)); |
| // TODO(b/298452477) Handle monochrome icons. |
| archivedActivity.monochromeIconBitmap = null; |
| activities.add(archivedActivity); |
| } |
| |
| if (activities.isEmpty()) { |
| throw new IllegalArgumentException( |
| "Failed to extract title and icon of main activities"); |
| } |
| |
| return activities.toArray(new ArchivedActivityParcel[activities.size()]); |
| } |
| } |