blob: 42a97f7e05b4bf19e54ec80bc8ab36951a6aec9c [file] [log] [blame]
/*
* 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.ComponentOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED;
import static android.content.pm.ArchivedActivity.bytesFromBitmap;
import static android.content.pm.ArchivedActivity.drawableToBitmap;
import static android.content.pm.PackageManager.DELETE_ARCHIVE;
import static android.content.pm.PackageManager.DELETE_KEEP_DATA;
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.content.Context;
import android.content.Intent;
import android.content.IntentSender;
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.ParceledListSlice;
import android.content.pm.ResolveInfo;
import android.content.pm.VersionedPackage;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Binder;
import android.os.Bundle;
import android.os.Environment;
import android.os.ParcelableException;
import android.os.Process;
import android.os.SELinux;
import android.os.UserHandle;
import android.text.TextUtils;
import android.util.Slog;
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.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";
/**
* 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 final Context mContext;
private final PackageManagerService mPm;
@Nullable
private LauncherApps mLauncherApps;
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) {
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.isRootOrShell(binderUid)) {
verifyCaller(snapshot.getPackageUid(callerPackageName, 0, userId), binderUid);
}
snapshot.enforceCrossUserPermission(binderUid, userId, true, true,
"archiveApp");
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,
intentSender,
userId,
binderUid);
})
.exceptionally(
e -> {
sendFailureStatus(intentSender, packageName, e.getMessage());
return null;
});
}
/** Creates archived state for the package and user. */
private CompletableFuture<ArchiveState> createArchiveState(String packageName, int userId)
throws PackageManager.NameNotFoundException {
PackageStateInternal ps = getPackageState(packageName, mPm.snapshotComputer(),
Binder.getCallingUid(), userId);
String responsibleInstallerPackage = getResponsibleInstallerPackage(ps);
verifyInstaller(responsibleInstallerPackage, userId);
List<LauncherActivityInfo> mainActivities = getLauncherActivityInfos(ps.getPackageName(),
userId);
final CompletableFuture<ArchiveState> archiveState = new CompletableFuture<>();
mPm.mHandler.post(() -> {
try {
archiveState.complete(
createArchiveStateInternal(packageName, userId, mainActivities,
responsibleInstallerPackage));
} catch (IOException e) {
archiveState.completeExceptionally(e);
}
});
return archiveState;
}
static ArchiveState createArchiveState(@NonNull ArchivedPackageParcel archivedPackage,
int userId, String installerPackage) {
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, installerPackage);
} catch (IOException e) {
Slog.e(TAG, "Failed to create archive state", e);
return null;
}
}
ArchiveState createArchiveStateInternal(String packageName, int userId,
List<LauncherActivityInfo> mainActivities, String installerPackage)
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, installerPackage);
}
// 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 installerPackage, int userId)
throws PackageManager.NameNotFoundException {
if (TextUtils.isEmpty(installerPackage)) {
throw new PackageManager.NameNotFoundException("No installer found");
}
// Allow shell for easier development.
if ((Binder.getCallingUid() != Process.SHELL_UID)
&& !verifySupportsUnarchival(installerPackage, 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();
}
void requestUnarchive(
@NonNull String packageName,
@NonNull String callerPackageName,
@NonNull UserHandle userHandle) {
Objects.requireNonNull(packageName);
Objects.requireNonNull(callerPackageName);
Objects.requireNonNull(userHandle);
Computer snapshot = mPm.snapshotComputer();
int userId = userHandle.getIdentifier();
int binderUid = Binder.getCallingUid();
if (!PackageManagerServiceUtils.isRootOrShell(binderUid)) {
verifyCaller(snapshot.getPackageUid(callerPackageName, 0, userId), binderUid);
}
snapshot.enforceCrossUserPermission(binderUid, userId, true, true,
"unarchiveApp");
PackageStateInternal ps;
try {
ps = getPackageState(packageName, 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)));
}
mPm.mHandler.post(() -> unarchiveInternal(packageName, userHandle, installerPackage));
}
/**
* 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);
snapshot.enforceCrossUserPermission(callingUid, userId, true, false,
"getArchivedAppIcon");
verifyArchived(ps, userId);
} catch (PackageManager.NameNotFoundException e) {
throw new ParcelableException(e);
}
List<ArchiveActivityInfo> activityInfos = ps.getUserStateOrDefault(
userId).getArchiveState().getActivityInfos();
if (activityInfos.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 decodeIcon(activityInfos.get(0));
}
@VisibleForTesting
Bitmap decodeIcon(ArchiveActivityInfo archiveActivityInfo) {
return BitmapFactory.decodeFile(archiveActivityInfo.getIconBitmap().toString());
}
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 userId = userHandle.getIdentifier();
Intent unarchiveIntent = new Intent(Intent.ACTION_UNARCHIVE_PACKAGE);
unarchiveIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
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;
}
@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 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 fillIn = new Intent();
fillIn.putExtra(PackageInstaller.EXTRA_PACKAGE_NAME, packageName);
fillIn.putExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE);
fillIn.putExtra(PackageInstaller.EXTRA_STATUS_MESSAGE, message);
try {
final BroadcastOptions options = BroadcastOptions.makeBasic();
options.setPendingIntentBackgroundActivityStartMode(
MODE_BACKGROUND_ACTIVITY_START_DENIED);
statusReceiver.sendIntent(mContext, 0, fillIn, /* onFinished= */ null,
/* handler= */ null, /* requiredPermission= */ null, options.toBundle());
} catch (IntentSender.SendIntentException e) {
Slog.e(
TAG,
TextUtils.formatSimple("Failed to send failure 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()));
}
/**
* 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()]);
}
}