blob: dea578fe595333266d4d5c8da2ebfa1caf8a9701 [file] [log] [blame]
/*
* Copyright (C) 2014 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.admin.DevicePolicyResources.Strings.Core.PACKAGE_DELETED_BY_DO;
import static android.content.pm.PackageInstaller.LOCATION_DATA_APP;
import static android.content.pm.PackageInstaller.UNARCHIVAL_ERROR_INSTALLER_DISABLED;
import static android.content.pm.PackageInstaller.UNARCHIVAL_ERROR_INSTALLER_UNINSTALLED;
import static android.content.pm.PackageInstaller.UNARCHIVAL_ERROR_INSUFFICIENT_STORAGE;
import static android.content.pm.PackageInstaller.UNARCHIVAL_ERROR_NO_CONNECTIVITY;
import static android.content.pm.PackageInstaller.UNARCHIVAL_ERROR_USER_ACTION_NEEDED;
import static android.content.pm.PackageInstaller.UNARCHIVAL_GENERIC_ERROR;
import static android.content.pm.PackageInstaller.UNARCHIVAL_OK;
import static android.content.pm.PackageManager.INSTALL_UNARCHIVE_DRAFT;
import static android.os.Process.INVALID_UID;
import static android.os.Process.SYSTEM_UID;
import static com.android.server.pm.PackageManagerService.SHELL_PACKAGE_NAME;
import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT;
import static org.xmlpull.v1.XmlPullParser.START_TAG;
import android.Manifest;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.AppGlobals;
import android.app.AppOpsManager;
import android.app.BroadcastOptions;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PackageDeleteObserver;
import android.app.PendingIntent;
import android.app.admin.DevicePolicyEventLogger;
import android.app.admin.DevicePolicyManager;
import android.app.admin.DevicePolicyManagerInternal;
import android.content.Context;
import android.content.Intent;
import android.content.IntentSender;
import android.content.IntentSender.SendIntentException;
import android.content.pm.ApplicationInfo;
import android.content.pm.ArchivedPackageParcel;
import android.content.pm.Flags;
import android.content.pm.IPackageInstaller;
import android.content.pm.IPackageInstallerCallback;
import android.content.pm.IPackageInstallerSession;
import android.content.pm.PackageInfo;
import android.content.pm.PackageInstaller;
import android.content.pm.PackageInstaller.InstallConstraints;
import android.content.pm.PackageInstaller.InstallConstraintsResult;
import android.content.pm.PackageInstaller.SessionInfo;
import android.content.pm.PackageInstaller.SessionParams;
import android.content.pm.PackageInstaller.UnarchivalStatus;
import android.content.pm.PackageItemInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.DeleteFlags;
import android.content.pm.ParceledListSlice;
import android.content.pm.VersionedPackage;
import android.content.pm.parsing.FrameworkParsingPackageUtils;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.os.ParcelableException;
import android.os.Process;
import android.os.RemoteCallback;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.os.SELinux;
import android.os.UserHandle;
import android.os.UserManager;
import android.os.storage.StorageManager;
import android.stats.devicepolicy.DevicePolicyEnums;
import android.system.ErrnoException;
import android.system.Os;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.util.ArraySet;
import android.util.AtomicFile;
import android.util.ExceptionUtils;
import android.util.Log;
import android.util.Slog;
import android.util.SparseArray;
import android.util.SparseBooleanArray;
import android.util.SparseIntArray;
import android.util.Xml;
import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.content.InstallLocationUtils;
import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
import com.android.internal.notification.SystemNotificationChannels;
import com.android.internal.util.ImageUtils;
import com.android.internal.util.IndentingPrintWriter;
import com.android.modules.utils.TypedXmlPullParser;
import com.android.modules.utils.TypedXmlSerializer;
import com.android.server.IoThread;
import com.android.server.LocalServices;
import com.android.server.SystemConfig;
import com.android.server.SystemService;
import com.android.server.SystemServiceManager;
import com.android.server.pm.parsing.PackageParser2;
import com.android.server.pm.pkg.PackageStateInternal;
import com.android.server.pm.utils.RequestThrottle;
import libcore.io.IoUtils;
import org.xmlpull.v1.XmlPullParserException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Random;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.concurrent.CompletableFuture;
import java.util.function.IntPredicate;
import java.util.function.Supplier;
/** The service responsible for installing packages. */
public class PackageInstallerService extends IPackageInstaller.Stub implements
PackageSessionProvider {
private static final String TAG = "PackageInstaller";
private static final boolean LOGD = Log.isLoggable(TAG, Log.DEBUG);
private static final boolean DEBUG = Build.IS_DEBUGGABLE;
// TODO: remove outstanding sessions when installer package goes away
// TODO: notify listeners in other users when package has been installed there
// TODO: purge expired sessions periodically in addition to at reboot
/** XML constants used in {@link #mSessionsFile} */
private static final String TAG_SESSIONS = "sessions";
/** Automatically destroy sessions older than this */
private static final long MAX_AGE_MILLIS = 3 * DateUtils.DAY_IN_MILLIS;
/** Automatically destroy staged sessions that have not changed state in this time */
private static final long MAX_TIME_SINCE_UPDATE_MILLIS = 21 * DateUtils.DAY_IN_MILLIS;
/** Upper bound on number of active sessions for a UID that has INSTALL_PACKAGES */
private static final long MAX_ACTIVE_SESSIONS_WITH_PERMISSION = 1024;
/** Upper bound on number of active sessions for a UID without INSTALL_PACKAGES */
private static final long MAX_ACTIVE_SESSIONS_NO_PERMISSION = 50;
/** Upper bound on number of historical sessions for a UID */
private static final long MAX_HISTORICAL_SESSIONS = 1048576;
/** Destroy sessions older than this on storage free request */
private static final long MAX_SESSION_AGE_ON_LOW_STORAGE_MILLIS = 8 * DateUtils.HOUR_IN_MILLIS;
/** Maximum time to wait for install constraints to be satisfied */
private static final long MAX_INSTALL_CONSTRAINTS_TIMEOUT_MILLIS = DateUtils.WEEK_IN_MILLIS;
/** Threshold of historical sessions size */
private static final int HISTORICAL_SESSIONS_THRESHOLD = 500;
/** Size of historical sessions to be cleared when reaching threshold */
private static final int HISTORICAL_CLEAR_SIZE = 400;
/**
* Allow verification-skipping if it's a development app installed through ADB with
* disable verification flag specified.
*/
private static final int ADB_DEV_MODE = PackageManager.INSTALL_FROM_ADB
| PackageManager.INSTALL_ALLOW_TEST;
/**
* Set of app op permissions that the installer of a session is allowed to change through
* {@link PackageInstaller.SessionParams#setPermissionState(String, int)}.
*/
public static final Set<String> INSTALLER_CHANGEABLE_APP_OP_PERMISSIONS = Set.of(
Manifest.permission.USE_FULL_SCREEN_INTENT
);
final PackageArchiver mPackageArchiver;
private final Context mContext;
private final PackageManagerService mPm;
private final ApexManager mApexManager;
private final StagingManager mStagingManager;
private AppOpsManager mAppOps;
private final HandlerThread mInstallThread;
private final Handler mInstallHandler;
private final Callbacks mCallbacks;
private volatile boolean mOkToSendBroadcasts = false;
private volatile boolean mBypassNextStagedInstallerCheck = false;
private volatile boolean mBypassNextAllowedApexUpdateCheck = false;
private volatile int mDisableVerificationForUid = INVALID_UID;
/**
* File storing persisted {@link #mSessions} metadata.
*/
private final AtomicFile mSessionsFile;
/**
* Directory storing persisted {@link #mSessions} metadata which is too
* heavy to store directly in {@link #mSessionsFile}.
*/
private final File mSessionsDir;
private final InternalCallback mInternalCallback = new InternalCallback();
private final PackageSessionVerifier mSessionVerifier;
private final GentleUpdateHelper mGentleUpdateHelper;
/**
* Used for generating session IDs. Since this is created at boot time,
* normal random might be predictable.
*/
private final Random mRandom = new SecureRandom();
/** All sessions allocated */
@GuardedBy("mSessions")
private final SparseBooleanArray mAllocatedSessions = new SparseBooleanArray();
@GuardedBy("mSessions")
private final SparseArray<PackageInstallerSession> mSessions = new SparseArray<>();
/** Historical sessions kept around for debugging purposes */
@GuardedBy("mSessions")
private final List<PackageInstallerHistoricalSession> mHistoricalSessions = new ArrayList<>();
@GuardedBy("mSessions")
private final SparseIntArray mHistoricalSessionsByInstaller = new SparseIntArray();
/** Sessions allocated to legacy users */
@GuardedBy("mSessions")
private final SparseBooleanArray mLegacySessions = new SparseBooleanArray();
/** Policy for allowing a silent update. */
private final SilentUpdatePolicy mSilentUpdatePolicy = new SilentUpdatePolicy();
private static final FilenameFilter sStageFilter = new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
return isStageName(name);
}
};
private static final class Lifecycle extends SystemService {
private final PackageInstallerService mPackageInstallerService;
Lifecycle(Context context, PackageInstallerService service) {
super(context);
mPackageInstallerService = service;
}
@Override
public void onStart() {
// no-op
}
@Override
public void onBootPhase(int phase) {
if (phase == SystemService.PHASE_ACTIVITY_MANAGER_READY) {
mPackageInstallerService.onBroadcastReady();
}
}
}
@NonNull
private final RequestThrottle mSettingsWriteRequest = new RequestThrottle(IoThread.getHandler(),
() -> {
synchronized (mSessions) {
return writeSessionsLocked();
}
});
public PackageInstallerService(Context context, PackageManagerService pm,
Supplier<PackageParser2> apexParserSupplier) {
mContext = context;
mPm = pm;
mInstallThread = new HandlerThread(TAG);
mInstallThread.start();
mInstallHandler = new Handler(mInstallThread.getLooper());
mCallbacks = new Callbacks(mInstallThread.getLooper());
mSessionsFile = new AtomicFile(
new File(Environment.getDataSystemDirectory(), "install_sessions.xml"),
"package-session");
mSessionsDir = new File(Environment.getDataSystemDirectory(), "install_sessions");
mSessionsDir.mkdirs();
mApexManager = ApexManager.getInstance();
mStagingManager = new StagingManager(context);
mSessionVerifier = new PackageSessionVerifier(context, mPm, mApexManager,
apexParserSupplier, mInstallThread.getLooper());
mGentleUpdateHelper = new GentleUpdateHelper(
context, mInstallThread.getLooper(), new AppStateHelper(context));
mPackageArchiver = new PackageArchiver(mContext, mPm);
LocalServices.getService(SystemServiceManager.class).startService(
new Lifecycle(context, this));
}
StagingManager getStagingManager() {
return mStagingManager;
}
boolean okToSendBroadcasts() {
return mOkToSendBroadcasts;
}
public void systemReady() {
mAppOps = mContext.getSystemService(AppOpsManager.class);
mStagingManager.systemReady();
mGentleUpdateHelper.systemReady();
synchronized (mSessions) {
readSessionsLocked();
expireSessionsLocked();
reconcileStagesLocked(StorageManager.UUID_PRIVATE_INTERNAL);
final ArraySet<File> unclaimedIcons = newArraySet(
mSessionsDir.listFiles());
// Ignore stages and icons claimed by active sessions
for (int i = 0; i < mSessions.size(); i++) {
final PackageInstallerSession session = mSessions.valueAt(i);
unclaimedIcons.remove(buildAppIconFile(session.sessionId));
}
// Clean up orphaned icons
for (File icon : unclaimedIcons) {
Slog.w(TAG, "Deleting orphan icon " + icon);
icon.delete();
}
// Invalid sessions might have been marked while parsing. Re-write the database with
// the updated information.
mSettingsWriteRequest.runNow();
}
}
private void onBroadcastReady() {
// Broadcasts are not sent while we restore sessions on boot, since no processes would be
// ready to listen to them. From now on, it is safe to send broadcasts which otherwise will
// be rejected by ActivityManagerService if its systemReady() is not completed.
mOkToSendBroadcasts = true;
}
void restoreAndApplyStagedSessionIfNeeded() {
List<StagingManager.StagedSession> stagedSessionsToRestore = new ArrayList<>();
synchronized (mSessions) {
for (int i = 0; i < mSessions.size(); i++) {
final PackageInstallerSession session = mSessions.valueAt(i);
if (!session.isStaged()) {
continue;
}
StagingManager.StagedSession stagedSession = session.mStagedSession;
if (!stagedSession.isInTerminalState() && stagedSession.hasParentSessionId()
&& getSession(stagedSession.getParentSessionId()) == null) {
stagedSession.setSessionFailed(PackageManager.INSTALL_ACTIVATION_FAILED,
"An orphan staged session " + stagedSession.sessionId() + " is found, "
+ "parent " + stagedSession.getParentSessionId() + " is missing");
continue;
}
if (!stagedSession.hasParentSessionId() && stagedSession.isCommitted()
&& !stagedSession.isInTerminalState()) {
// StagingManager.restoreSessions expects a list of committed, non-finalized
// parent staged sessions.
stagedSessionsToRestore.add(stagedSession);
}
}
}
// Don't hold mSessions lock when calling restoreSessions, since it might trigger an APK
// atomic install which needs to query sessions, which requires lock on mSessions.
// Note: restoreSessions mutates content of stagedSessionsToRestore.
mStagingManager.restoreSessions(stagedSessionsToRestore, mPm.isDeviceUpgrading());
}
@GuardedBy("mSessions")
private void reconcileStagesLocked(String volumeUuid) {
final ArraySet<File> unclaimedStages = getStagingDirsOnVolume(volumeUuid);
// Ignore stages claimed by active sessions
for (int i = 0; i < mSessions.size(); i++) {
final PackageInstallerSession session = mSessions.valueAt(i);
unclaimedStages.remove(session.stageDir);
}
removeStagingDirs(unclaimedStages);
}
private ArraySet<File> getStagingDirsOnVolume(String volumeUuid) {
final File stagingDir = getTmpSessionDir(volumeUuid);
final ArraySet<File> stagingDirs = newArraySet(stagingDir.listFiles(sStageFilter));
// We also need to clean up orphaned staging directory for staged sessions
final File stagedSessionStagingDir = Environment.getDataStagingDirectory(volumeUuid);
stagingDirs.addAll(newArraySet(stagedSessionStagingDir.listFiles()));
return stagingDirs;
}
private void removeStagingDirs(ArraySet<File> stagingDirsToRemove) {
// Clean up orphaned staging directories
for (File stage : stagingDirsToRemove) {
Slog.w(TAG, "Deleting orphan stage " + stage);
mPm.removeCodePath(stage);
}
}
public void onPrivateVolumeMounted(String volumeUuid) {
synchronized (mSessions) {
reconcileStagesLocked(volumeUuid);
}
}
/**
* Called to free up some storage space from obsolete installation files
*/
public void freeStageDirs(String volumeUuid) {
final ArraySet<File> unclaimedStagingDirsOnVolume = getStagingDirsOnVolume(volumeUuid);
final long currentTimeMillis = System.currentTimeMillis();
synchronized (mSessions) {
for (int i = 0; i < mSessions.size(); i++) {
final PackageInstallerSession session = mSessions.valueAt(i);
if (!unclaimedStagingDirsOnVolume.contains(session.stageDir)) {
// Only handles sessions stored on the target volume
continue;
}
final long age = currentTimeMillis - session.createdMillis;
if (age >= MAX_SESSION_AGE_ON_LOW_STORAGE_MILLIS) {
// Aggressively close old sessions because we are running low on storage
// Their staging dirs will be removed too
PackageInstallerSession root = !session.hasParentSessionId()
? session : mSessions.get(session.getParentSessionId());
if (root == null) {
Slog.e(TAG, "freeStageDirs: found an orphaned session: "
+ session.sessionId + " parent=" + session.getParentSessionId());
} else if (!root.isDestroyed()) {
root.abandon();
}
} else {
// Session is new enough, so it deserves to be kept even on low storage
unclaimedStagingDirsOnVolume.remove(session.stageDir);
}
}
}
removeStagingDirs(unclaimedStagingDirsOnVolume);
}
@Deprecated
public File allocateStageDirLegacy(String volumeUuid, boolean isEphemeral) throws IOException {
synchronized (mSessions) {
try {
final int sessionId = allocateSessionIdLocked();
mLegacySessions.put(sessionId, true);
final File sessionStageDir = buildTmpSessionDir(sessionId, volumeUuid);
prepareStageDir(sessionStageDir);
return sessionStageDir;
} catch (IllegalStateException e) {
throw new IOException(e);
}
}
}
@Deprecated
public String allocateExternalStageCidLegacy() {
synchronized (mSessions) {
final int sessionId = allocateSessionIdLocked();
mLegacySessions.put(sessionId, true);
return "smdl" + sessionId + ".tmp";
}
}
@GuardedBy("mSessions")
private void readSessionsLocked() {
if (LOGD) Slog.v(TAG, "readSessionsLocked()");
mSessions.clear();
FileInputStream fis = null;
try {
fis = mSessionsFile.openRead();
final TypedXmlPullParser in = Xml.resolvePullParser(fis);
int type;
while ((type = in.next()) != END_DOCUMENT) {
if (type == START_TAG) {
final String tag = in.getName();
if (PackageInstallerSession.TAG_SESSION.equals(tag)) {
final PackageInstallerSession session;
try {
session = PackageInstallerSession.readFromXml(in, mInternalCallback,
mContext, mPm, mInstallThread.getLooper(), mStagingManager,
mSessionsDir, this, mSilentUpdatePolicy);
} catch (Exception e) {
Slog.e(TAG, "Could not read session", e);
continue;
}
mSessions.put(session.sessionId, session);
mAllocatedSessions.put(session.sessionId, true);
}
}
}
} catch (FileNotFoundException e) {
// Missing sessions are okay, probably first boot
} catch (IOException | XmlPullParserException | ArrayIndexOutOfBoundsException e) {
Slog.wtf(TAG, "Failed reading install sessions", e);
} finally {
IoUtils.closeQuietly(fis);
}
// After reboot housekeeping.
for (int i = 0; i < mSessions.size(); ++i) {
PackageInstallerSession session = mSessions.valueAt(i);
session.onAfterSessionRead(mSessions);
}
}
@GuardedBy("mSessions")
private void expireSessionsLocked() {
SparseArray<PackageInstallerSession> tmp = mSessions.clone();
final int n = tmp.size();
for (int i = 0; i < n; ++i) {
PackageInstallerSession session = tmp.valueAt(i);
if (session.hasParentSessionId()) {
// Child sessions will be expired when handling parent sessions
continue;
}
final long age = System.currentTimeMillis() - session.createdMillis;
final long timeSinceUpdate = System.currentTimeMillis() - session.getUpdatedMillis();
final boolean valid;
if (session.isStaged()) {
valid = !session.isStagedAndInTerminalState()
|| timeSinceUpdate < MAX_TIME_SINCE_UPDATE_MILLIS;
} else if (age >= MAX_AGE_MILLIS) {
Slog.w(TAG, "Abandoning old session created at "
+ session.createdMillis);
valid = false;
} else {
valid = true;
}
if (!valid) {
Slog.w(TAG, "Remove old session: " + session.sessionId);
// Remove expired sessions as well as child sessions if any
removeActiveSession(session);
}
}
}
/**
* Moves a session (including the child sessions) from mSessions to mHistoricalSessions.
* This should only be called on a root session.
*/
@GuardedBy("mSessions")
private void removeActiveSession(PackageInstallerSession session) {
mSessions.remove(session.sessionId);
addHistoricalSessionLocked(session);
for (PackageInstallerSession child : session.getChildSessions()) {
mSessions.remove(child.sessionId);
addHistoricalSessionLocked(child);
}
}
@GuardedBy("mSessions")
private void addHistoricalSessionLocked(PackageInstallerSession session) {
if (mHistoricalSessions.size() > HISTORICAL_SESSIONS_THRESHOLD) {
Slog.d(TAG, "Historical sessions size reaches threshold, clear the oldest");
mHistoricalSessions.subList(0, HISTORICAL_CLEAR_SIZE).clear();
}
mHistoricalSessions.add(session.createHistoricalSession());
int installerUid = session.getInstallerUid();
// Increment the number of sessions by this installerUid.
mHistoricalSessionsByInstaller.put(installerUid,
mHistoricalSessionsByInstaller.get(installerUid) + 1);
}
@GuardedBy("mSessions")
private boolean writeSessionsLocked() {
if (LOGD) Slog.v(TAG, "writeSessionsLocked()");
FileOutputStream fos = null;
try {
fos = mSessionsFile.startWrite();
final TypedXmlSerializer out = Xml.resolveSerializer(fos);
out.startDocument(null, true);
out.startTag(null, TAG_SESSIONS);
final int size = mSessions.size();
for (int i = 0; i < size; i++) {
final PackageInstallerSession session = mSessions.valueAt(i);
session.write(out, mSessionsDir);
}
out.endTag(null, TAG_SESSIONS);
out.endDocument();
mSessionsFile.finishWrite(fos);
return true;
} catch (IOException e) {
if (fos != null) {
mSessionsFile.failWrite(fos);
}
}
return false;
}
private File buildAppIconFile(int sessionId) {
return new File(mSessionsDir, "app_icon." + sessionId + ".png");
}
@Override
public int createSession(SessionParams params, String installerPackageName,
String callingAttributionTag, int userId) {
try {
if (params.dataLoaderParams != null
&& mContext.checkCallingOrSelfPermission(Manifest.permission.USE_INSTALLER_V2)
!= PackageManager.PERMISSION_GRANTED) {
throw new SecurityException("You need the "
+ "com.android.permission.USE_INSTALLER_V2 permission "
+ "to use a data loader");
}
// Draft sessions cannot be created through the public API.
params.installFlags &= ~PackageManager.INSTALL_UNARCHIVE_DRAFT;
return createSessionInternal(params, installerPackageName, callingAttributionTag,
Binder.getCallingUid(), userId);
} catch (IOException e) {
throw ExceptionUtils.wrap(e);
}
}
int createSessionInternal(SessionParams params, String installerPackageName,
String installerAttributionTag, int callingUid, int userId)
throws IOException {
final Computer snapshot = mPm.snapshotComputer();
snapshot.enforceCrossUserPermission(callingUid, userId, true, true, "createSession");
if (mPm.isUserRestricted(userId, UserManager.DISALLOW_INSTALL_APPS)) {
throw new SecurityException("User restriction prevents installing");
}
// INSTALL_REASON_ROLLBACK allows an app to be rolled back without requiring the ROLLBACK
// capability; ensure if this is set as the install reason the app has one of the necessary
// signature permissions to perform the rollback.
if (params.installReason == PackageManager.INSTALL_REASON_ROLLBACK) {
if (mContext.checkCallingOrSelfPermission(Manifest.permission.MANAGE_ROLLBACKS)
!= PackageManager.PERMISSION_GRANTED &&
mContext.checkCallingOrSelfPermission(Manifest.permission.TEST_MANAGE_ROLLBACKS)
!= PackageManager.PERMISSION_GRANTED) {
throw new SecurityException(
"INSTALL_REASON_ROLLBACK requires the MANAGE_ROLLBACKS permission or the "
+ "TEST_MANAGE_ROLLBACKS permission");
}
}
// App package name and label length is restricted so that really long strings aren't
// written to disk.
if (params.appPackageName != null && !isValidPackageName(params.appPackageName)) {
params.appPackageName = null;
}
params.appLabel = TextUtils.trimToSize(params.appLabel,
PackageItemInfo.MAX_SAFE_LABEL_LENGTH);
// Validate installer package name.
if (params.installerPackageName != null && !isValidPackageName(
params.installerPackageName)) {
params.installerPackageName = null;
}
var requestedInstallerPackageName =
params.installerPackageName != null ? params.installerPackageName
: installerPackageName;
if (PackageManagerServiceUtils.isRootOrShell(callingUid)
|| PackageInstallerSession.isSystemDataLoaderInstallation(params)
|| PackageManagerServiceUtils.isAdoptedShell(callingUid, mContext)) {
params.installFlags |= PackageManager.INSTALL_FROM_ADB;
// adb installs can override the installingPackageName, but not the
// initiatingPackageName
installerPackageName = SHELL_PACKAGE_NAME;
} else {
if (callingUid != SYSTEM_UID) {
// The supplied installerPackageName must always belong to the calling app.
mAppOps.checkPackage(callingUid, installerPackageName);
}
// Only apps with INSTALL_PACKAGES are allowed to set an installer that is not the
// caller.
if (!TextUtils.equals(requestedInstallerPackageName, installerPackageName)) {
if (mContext.checkCallingOrSelfPermission(Manifest.permission.INSTALL_PACKAGES)
!= PackageManager.PERMISSION_GRANTED) {
mAppOps.checkPackage(callingUid, requestedInstallerPackageName);
}
}
params.installFlags &= ~PackageManager.INSTALL_FROM_ADB;
params.installFlags &= ~PackageManager.INSTALL_ALL_USERS;
params.installFlags &= ~PackageManager.INSTALL_ARCHIVED;
params.installFlags |= PackageManager.INSTALL_REPLACE_EXISTING;
if ((params.installFlags & PackageManager.INSTALL_VIRTUAL_PRELOAD) != 0
&& !mPm.isCallerVerifier(snapshot, callingUid)) {
params.installFlags &= ~PackageManager.INSTALL_VIRTUAL_PRELOAD;
}
if (mContext.checkCallingOrSelfPermission(Manifest.permission.INSTALL_TEST_ONLY_PACKAGE)
!= PackageManager.PERMISSION_GRANTED) {
params.installFlags &= ~PackageManager.INSTALL_ALLOW_TEST;
}
// developmentInstallFlags can ony be set by shell or root.
params.developmentInstallFlags = 0;
}
String originatingPackageName = null;
if (params.originatingUid != SessionParams.UID_UNKNOWN
&& params.originatingUid != callingUid) {
String[] packages = snapshot.getPackagesForUid(params.originatingUid);
if (packages != null && packages.length > 0) {
// Choose an arbitrary representative package in the case of a shared UID.
originatingPackageName = packages[0];
}
}
if (Build.IS_DEBUGGABLE || PackageManagerServiceUtils.isSystemOrRoot(callingUid)) {
params.installFlags |= PackageManager.INSTALL_ALLOW_DOWNGRADE;
} else {
params.installFlags &= ~PackageManager.INSTALL_ALLOW_DOWNGRADE;
}
if (mDisableVerificationForUid != INVALID_UID) {
if (callingUid == mDisableVerificationForUid) {
params.installFlags |= PackageManager.INSTALL_DISABLE_VERIFICATION;
} else {
// Clear the flag if current calling uid doesn't match the requested uid.
params.installFlags &= ~PackageManager.INSTALL_DISABLE_VERIFICATION;
}
// Reset the field as this is a one-off request.
mDisableVerificationForUid = INVALID_UID;
} else if ((params.installFlags & ADB_DEV_MODE) != ADB_DEV_MODE) {
// Only tools under specific conditions (test app installed through ADB, and
// verification disabled flag specified) can disable verification.
params.installFlags &= ~PackageManager.INSTALL_DISABLE_VERIFICATION;
}
if (Flags.rollbackLifetime()) {
if (params.rollbackLifetimeMillis > 0) {
if ((params.installFlags & PackageManager.INSTALL_ENABLE_ROLLBACK) == 0) {
throw new IllegalArgumentException(
"Can't set rollbackLifetimeMillis when rollback is not enabled");
}
if (mContext.checkCallingOrSelfPermission(Manifest.permission.MANAGE_ROLLBACKS)
!= PackageManager.PERMISSION_GRANTED) {
throw new SecurityException(
"Setting rollback lifetime requires the MANAGE_ROLLBACKS permission");
}
} else if (params.rollbackLifetimeMillis < 0) {
throw new IllegalArgumentException("rollbackLifetimeMillis can't be negative.");
}
}
if (Flags.recoverabilityDetection()) {
if (params.rollbackImpactLevel == PackageManager.ROLLBACK_USER_IMPACT_HIGH
|| params.rollbackImpactLevel
== PackageManager.ROLLBACK_USER_IMPACT_ONLY_MANUAL) {
if ((params.installFlags & PackageManager.INSTALL_ENABLE_ROLLBACK) == 0) {
throw new IllegalArgumentException(
"Can't set rollbackImpactLevel when rollback is not enabled");
}
if (mContext.checkCallingOrSelfPermission(Manifest.permission.MANAGE_ROLLBACKS)
!= PackageManager.PERMISSION_GRANTED) {
throw new SecurityException(
"Setting rollbackImpactLevel requires the MANAGE_ROLLBACKS permission");
}
} else if (params.rollbackImpactLevel < 0) {
throw new IllegalArgumentException("rollbackImpactLevel can't be negative.");
}
}
boolean isApex = (params.installFlags & PackageManager.INSTALL_APEX) != 0;
if (isApex) {
if (mContext.checkCallingOrSelfPermission(Manifest.permission.INSTALL_PACKAGE_UPDATES)
== PackageManager.PERMISSION_DENIED
&& mContext.checkCallingOrSelfPermission(Manifest.permission.INSTALL_PACKAGES)
== PackageManager.PERMISSION_DENIED) {
throw new SecurityException("Not allowed to perform APEX updates");
}
} else if (params.isStaged) {
mContext.enforceCallingOrSelfPermission(Manifest.permission.INSTALL_PACKAGES, TAG);
}
if (isApex) {
if (!mApexManager.isApexSupported()) {
throw new IllegalArgumentException(
"This device doesn't support the installation of APEX files");
}
if (params.isMultiPackage) {
throw new IllegalArgumentException("A multi-session can't be set as APEX.");
}
if (PackageManagerServiceUtils.isSystemOrRootOrShell(callingUid)
|| mBypassNextAllowedApexUpdateCheck) {
params.installFlags |= PackageManager.INSTALL_DISABLE_ALLOWED_APEX_UPDATE_CHECK;
} else {
// Only specific APEX updates (installed through ADB, or for CTS tests) can disable
// allowed APEX update check.
params.installFlags &= ~PackageManager.INSTALL_DISABLE_ALLOWED_APEX_UPDATE_CHECK;
}
}
if ((params.installFlags & PackageManager.INSTALL_BYPASS_LOW_TARGET_SDK_BLOCK) != 0
&& !PackageManagerServiceUtils.isSystemOrRootOrShell(callingUid)
&& !Build.IS_DEBUGGABLE) {
// If the bypass flag is set, but not running as system root or shell then remove
// the flag
params.installFlags &= ~PackageManager.INSTALL_BYPASS_LOW_TARGET_SDK_BLOCK;
}
params.installFlags &= ~PackageManager.INSTALL_UNARCHIVE;
if (Flags.archiving() && params.appPackageName != null) {
PackageStateInternal ps = mPm.snapshotComputer().getPackageStateInternal(
params.appPackageName, SYSTEM_UID);
if (ps != null
&& PackageArchiver.isArchived(ps.getUserStateOrDefault(userId))
&& PackageArchiver.getResponsibleInstallerPackage(ps)
.equals(requestedInstallerPackageName)) {
params.installFlags |= PackageManager.INSTALL_UNARCHIVE;
}
}
if ((params.installFlags & PackageManager.INSTALL_INSTANT_APP) != 0
&& !PackageManagerServiceUtils.isSystemOrRootOrShell(callingUid)
&& (snapshot.getFlagsForUid(callingUid) & ApplicationInfo.FLAG_SYSTEM)
== 0) {
throw new SecurityException(
"Only system apps could use the PackageManager.INSTALL_INSTANT_APP flag.");
}
if (params.isStaged && !PackageManagerServiceUtils.isSystemOrRootOrShell(callingUid)) {
if (!mBypassNextStagedInstallerCheck
&& !isStagedInstallerAllowed(requestedInstallerPackageName)) {
throw new SecurityException("Installer not allowed to commit staged install");
}
}
if (isApex && !PackageManagerServiceUtils.isSystemOrRootOrShell(callingUid)) {
if (!mBypassNextStagedInstallerCheck
&& !isStagedInstallerAllowed(requestedInstallerPackageName)) {
throw new SecurityException(
"Installer not allowed to commit non-staged APEX install");
}
}
mBypassNextStagedInstallerCheck = false;
mBypassNextAllowedApexUpdateCheck = false;
if (!params.isMultiPackage) {
var hasInstallGrantRuntimePermissions = mContext.checkCallingOrSelfPermission(
Manifest.permission.INSTALL_GRANT_RUNTIME_PERMISSIONS)
== PackageManager.PERMISSION_GRANTED;
// Only system components can circumvent runtime permissions when installing.
if ((params.installFlags & PackageManager.INSTALL_GRANT_ALL_REQUESTED_PERMISSIONS) != 0
&& !hasInstallGrantRuntimePermissions) {
throw new SecurityException("You need the "
+ Manifest.permission.INSTALL_GRANT_RUNTIME_PERMISSIONS
+ " permission to use the"
+ " PackageManager.INSTALL_GRANT_ALL_REQUESTED_PERMISSIONS flag");
}
var permissionStates = params.getPermissionStates();
if (!permissionStates.isEmpty()) {
if (!hasInstallGrantRuntimePermissions) {
for (int index = 0; index < permissionStates.size(); index++) {
var permissionName = permissionStates.keyAt(index);
if (!INSTALLER_CHANGEABLE_APP_OP_PERMISSIONS.contains(permissionName)) {
throw new SecurityException("You need the "
+ Manifest.permission.INSTALL_GRANT_RUNTIME_PERMISSIONS
+ " permission to grant runtime permissions for a session");
}
}
}
}
// Defensively resize giant app icons
if (params.appIcon != null) {
final ActivityManager am = (ActivityManager) mContext.getSystemService(
Context.ACTIVITY_SERVICE);
final int iconSize = am.getLauncherLargeIconSize();
if ((params.appIcon.getWidth() > iconSize * 2)
|| (params.appIcon.getHeight() > iconSize * 2)) {
params.appIcon = Bitmap.createScaledBitmap(params.appIcon, iconSize, iconSize,
true);
}
}
switch (params.mode) {
case SessionParams.MODE_FULL_INSTALL:
case SessionParams.MODE_INHERIT_EXISTING:
break;
default:
throw new IllegalArgumentException("Invalid install mode: " + params.mode);
}
// If caller requested explicit location, validity check it, otherwise
// resolve the best internal or adopted location.
if ((params.installFlags & PackageManager.INSTALL_INTERNAL) != 0) {
if (!InstallLocationUtils.fitsOnInternal(mContext, params)) {
throw new IOException("No suitable internal storage available");
}
} else if ((params.installFlags & PackageManager.INSTALL_FORCE_VOLUME_UUID) != 0) {
// For now, installs to adopted media are treated as internal from
// an install flag point-of-view.
params.installFlags |= PackageManager.INSTALL_INTERNAL;
} else {
params.installFlags |= PackageManager.INSTALL_INTERNAL;
// Resolve best location for install, based on combination of
// requested install flags, delta size, and manifest settings.
final long ident = Binder.clearCallingIdentity();
try {
params.volumeUuid = InstallLocationUtils.resolveInstallVolume(mContext, params);
} finally {
Binder.restoreCallingIdentity(ident);
}
}
}
int requestedInstallerPackageUid = INVALID_UID;
if (requestedInstallerPackageName != null) {
requestedInstallerPackageUid = snapshot.getPackageUid(requestedInstallerPackageName,
0 /* flags */, userId);
}
if (requestedInstallerPackageUid == INVALID_UID) {
// Requested installer package is invalid, reset it
requestedInstallerPackageName = null;
}
final int sessionId;
final PackageInstallerSession session;
synchronized (mSessions) {
// Check that the installer does not have too many active sessions.
final int activeCount = getSessionCount(mSessions, callingUid);
if (mContext.checkCallingOrSelfPermission(Manifest.permission.INSTALL_PACKAGES)
== PackageManager.PERMISSION_GRANTED) {
if (activeCount >= MAX_ACTIVE_SESSIONS_WITH_PERMISSION) {
throw new IllegalStateException(
"Too many active sessions for UID " + callingUid);
}
} else if (activeCount >= MAX_ACTIVE_SESSIONS_NO_PERMISSION) {
throw new IllegalStateException(
"Too many active sessions for UID " + callingUid);
}
final int historicalCount = mHistoricalSessionsByInstaller.get(callingUid);
if (historicalCount >= MAX_HISTORICAL_SESSIONS) {
throw new IllegalStateException(
"Too many historical sessions for UID " + callingUid);
}
final int existingDraftSessionId =
getExistingDraftSessionId(requestedInstallerPackageUid, params, userId);
sessionId = existingDraftSessionId != SessionInfo.INVALID_ID ? existingDraftSessionId
: allocateSessionIdLocked();
}
final long createdMillis = System.currentTimeMillis();
// We're staging to exactly one location
File stageDir = null;
String stageCid = null;
if (!params.isMultiPackage) {
if ((params.installFlags & PackageManager.INSTALL_INTERNAL) != 0) {
stageDir = buildSessionDir(sessionId, params);
} else {
stageCid = buildExternalStageCid(sessionId);
}
}
// reset the force queryable param if it's not called by an approved caller.
if (params.forceQueryableOverride) {
if (!PackageManagerServiceUtils.isRootOrShell(callingUid)) {
params.forceQueryableOverride = false;
}
}
final var dpmi = LocalServices.getService(DevicePolicyManagerInternal.class);
if (dpmi != null && dpmi.isUserOrganizationManaged(userId)) {
params.installFlags |= PackageManager.INSTALL_FROM_MANAGED_USER_OR_PROFILE;
}
if (isApex || mContext.checkCallingOrSelfPermission(
Manifest.permission.ENFORCE_UPDATE_OWNERSHIP) == PackageManager.PERMISSION_DENIED) {
params.installFlags &= ~PackageManager.INSTALL_REQUEST_UPDATE_OWNERSHIP;
}
InstallSource installSource = InstallSource.create(installerPackageName,
originatingPackageName, requestedInstallerPackageName, requestedInstallerPackageUid,
requestedInstallerPackageName, installerAttributionTag, params.packageSource);
session = new PackageInstallerSession(mInternalCallback, mContext, mPm, this,
mSilentUpdatePolicy, mInstallThread.getLooper(), mStagingManager, sessionId,
userId, callingUid, installSource, params, createdMillis, 0L, stageDir, stageCid,
null, null, false, false, false, false, null, SessionInfo.INVALID_ID,
false, false, false, PackageManager.INSTALL_UNKNOWN, "");
synchronized (mSessions) {
mSessions.put(sessionId, session);
}
mPm.addInstallerPackageName(session.getInstallSource());
mCallbacks.notifySessionCreated(session.sessionId, session.userId);
mSettingsWriteRequest.schedule();
if (LOGD) {
Slog.d(TAG, "Created session id=" + sessionId + " staged=" + params.isStaged);
}
return sessionId;
}
int getExistingDraftSessionId(int installerUid,
@NonNull SessionParams sessionParams, int userId) {
synchronized (mSessions) {
return getExistingDraftSessionIdInternal(installerUid, sessionParams, userId);
}
}
@GuardedBy("mSessions")
private int getExistingDraftSessionIdInternal(int installerUid,
SessionParams sessionParams, int userId) {
String appPackageName = sessionParams.appPackageName;
if (!Flags.archiving() || installerUid == INVALID_UID || appPackageName == null) {
return SessionInfo.INVALID_ID;
}
PackageStateInternal ps = mPm.snapshotComputer().getPackageStateInternal(appPackageName,
SYSTEM_UID);
if (ps == null || !PackageArchiver.isArchived(ps.getUserStateOrDefault(userId))) {
return SessionInfo.INVALID_ID;
}
// If unarchiveId is present we match based on it. If unarchiveId is missing we
// choose a draft session too to ensure we don't end up with duplicate sessions
// if the installer doesn't set this field.
if (sessionParams.unarchiveId > 0) {
PackageInstallerSession session = mSessions.get(sessionParams.unarchiveId);
if (session != null
&& isValidDraftSession(session, appPackageName, installerUid, userId)) {
return session.sessionId;
}
return SessionInfo.INVALID_ID;
}
for (int i = 0; i < mSessions.size(); i++) {
PackageInstallerSession session = mSessions.valueAt(i);
if (session != null
&& isValidDraftSession(session, appPackageName, installerUid, userId)) {
return session.sessionId;
}
}
return SessionInfo.INVALID_ID;
}
private boolean isValidDraftSession(@NonNull PackageInstallerSession session,
@NonNull String appPackageName, int installerUid, int userId) {
return (session.getInstallFlags() & PackageManager.INSTALL_UNARCHIVE_DRAFT) != 0
&& appPackageName.equals(session.params.appPackageName)
&& session.userId == userId
&& installerUid == session.getInstallerUid();
}
void cleanupDraftIfUnclaimed(int sessionId) {
synchronized (mSessions) {
PackageInstallerSession session = mPm.mInstallerService.getSession(sessionId);
if (session != null && (session.getInstallFlags() & INSTALL_UNARCHIVE_DRAFT) != 0) {
session.abandon();
}
}
}
private boolean isStagedInstallerAllowed(String installerName) {
return SystemConfig.getInstance().getWhitelistedStagedInstallers().contains(installerName);
}
@Override
public void updateSessionAppIcon(int sessionId, Bitmap appIcon) {
synchronized (mSessions) {
final PackageInstallerSession session = mSessions.get(sessionId);
if (session == null || !isCallingUidOwner(session)) {
throw new SecurityException("Caller has no access to session " + sessionId);
}
// Defensively resize giant app icons
if (appIcon != null) {
final ActivityManager am = (ActivityManager) mContext.getSystemService(
Context.ACTIVITY_SERVICE);
final int iconSize = am.getLauncherLargeIconSize();
if ((appIcon.getWidth() > iconSize * 2)
|| (appIcon.getHeight() > iconSize * 2)) {
appIcon = Bitmap.createScaledBitmap(appIcon, iconSize, iconSize, true);
}
}
session.params.appIcon = appIcon;
session.params.appIconLastModified = -1;
mInternalCallback.onSessionBadgingChanged(session);
}
}
@Override
public void updateSessionAppLabel(int sessionId, String appLabel) {
synchronized (mSessions) {
final PackageInstallerSession session = mSessions.get(sessionId);
if (session == null || !isCallingUidOwner(session)) {
throw new SecurityException("Caller has no access to session " + sessionId);
}
if (!appLabel.equals(session.params.appLabel)) {
session.params.appLabel = appLabel;
mInternalCallback.onSessionBadgingChanged(session);
}
}
}
@Override
public void abandonSession(int sessionId) {
synchronized (mSessions) {
final PackageInstallerSession session = mSessions.get(sessionId);
if (session == null || !isCallingUidOwner(session)) {
throw new SecurityException("Caller has no access to session " + sessionId);
}
session.abandon();
}
}
@Override
public IPackageInstallerSession openSession(int sessionId) {
try {
return openSessionInternal(sessionId);
} catch (IOException e) {
throw ExceptionUtils.wrap(e);
}
}
private boolean checkOpenSessionAccess(final PackageInstallerSession session) {
if (session == null
|| (session.getInstallFlags() & PackageManager.INSTALL_UNARCHIVE_DRAFT) != 0) {
return false;
}
if (isCallingUidOwner(session)) {
return true;
}
// Package verifiers have access to openSession for sealed sessions.
if (session.isSealed() && mContext.checkCallingOrSelfPermission(
android.Manifest.permission.PACKAGE_VERIFICATION_AGENT)
== PackageManager.PERMISSION_GRANTED) {
return true;
}
return false;
}
private PackageInstallerSession openSessionInternal(int sessionId) throws IOException {
synchronized (mSessions) {
final PackageInstallerSession session = mSessions.get(sessionId);
if (!checkOpenSessionAccess(session)) {
throw new SecurityException("Caller has no access to session " + sessionId);
}
session.open();
return session;
}
}
@GuardedBy("mSessions")
private int allocateSessionIdLocked() {
int n = 0;
int sessionId;
do {
sessionId = mRandom.nextInt(Integer.MAX_VALUE - 1) + 1;
if (!mAllocatedSessions.get(sessionId, false)) {
mAllocatedSessions.put(sessionId, true);
return sessionId;
}
} while (n++ < 32);
throw new IllegalStateException("Failed to allocate session ID");
}
static boolean isStageName(String name) {
final boolean isFile = name.startsWith("vmdl") && name.endsWith(".tmp");
final boolean isContainer = name.startsWith("smdl") && name.endsWith(".tmp");
final boolean isLegacyContainer = name.startsWith("smdl2tmp");
return isFile || isContainer || isLegacyContainer;
}
static int tryParseSessionId(@NonNull String tmpSessionDir)
throws IllegalArgumentException {
if (!tmpSessionDir.startsWith("vmdl") || !tmpSessionDir.endsWith(".tmp")) {
throw new IllegalArgumentException("Not a temporary session directory");
}
String sessionId = tmpSessionDir.substring("vmdl".length(),
tmpSessionDir.length() - ".tmp".length());
return Integer.parseInt(sessionId);
}
private static boolean isValidPackageName(@NonNull String packageName) {
if (packageName.length() > SessionParams.MAX_PACKAGE_NAME_LENGTH) {
return false;
}
// "android" is a valid package name
var errorMessage = FrameworkParsingPackageUtils.validateName(
packageName, /* requireSeparator= */ false, /* requireFilename */ true);
if (errorMessage != null) {
return false;
}
return true;
}
private File getTmpSessionDir(String volumeUuid) {
return Environment.getDataAppDirectory(volumeUuid);
}
private File buildTmpSessionDir(int sessionId, String volumeUuid) {
final File sessionStagingDir = getTmpSessionDir(volumeUuid);
return new File(sessionStagingDir, "vmdl" + sessionId + ".tmp");
}
private File buildSessionDir(int sessionId, SessionParams params) {
if (params.isStaged || (params.installFlags & PackageManager.INSTALL_APEX) != 0) {
final File sessionStagingDir = Environment.getDataStagingDirectory(params.volumeUuid);
return new File(sessionStagingDir, "session_" + sessionId);
}
final File result = buildTmpSessionDir(sessionId, params.volumeUuid);
if (DEBUG && !Objects.equals(tryParseSessionId(result.getName()), sessionId)) {
throw new RuntimeException(
"session folder format is off: " + result.getName() + " (" + sessionId + ")");
}
return result;
}
static void prepareStageDir(File stageDir) throws IOException {
if (stageDir.exists()) {
throw new IOException("Session dir already exists: " + stageDir);
}
try {
Os.mkdir(stageDir.getAbsolutePath(), 0775);
Os.chmod(stageDir.getAbsolutePath(), 0775);
} catch (ErrnoException e) {
// This purposefully throws if directory already exists
throw new IOException("Failed to prepare session dir: " + stageDir, e);
}
if (!SELinux.restorecon(stageDir)) {
String path = stageDir.getCanonicalPath();
String ctx = SELinux.fileSelabelLookup(path);
boolean success = SELinux.setFileContext(path, ctx);
Slog.e(TAG,
"Failed to SELinux.restorecon session dir, path: [" + path + "], ctx: [" + ctx
+ "]. Retrying via SELinux.fileSelabelLookup/SELinux.setFileContext: "
+ (success ? "SUCCESS" : "FAILURE"));
if (!success) {
throw new IOException("Failed to restorecon session dir: " + stageDir);
}
}
}
private String buildExternalStageCid(int sessionId) {
return "smdl" + sessionId + ".tmp";
}
private boolean shouldFilterSession(@NonNull Computer snapshot, int uid, SessionInfo info) {
if (info == null) {
return false;
}
return uid != info.getInstallerUid()
&& !snapshot.canQueryPackage(uid, info.getAppPackageName());
}
@Override
public SessionInfo getSessionInfo(int sessionId) {
final int callingUid = Binder.getCallingUid();
final SessionInfo result;
synchronized (mSessions) {
final PackageInstallerSession session = mSessions.get(sessionId);
result = (session != null && !(session.isStaged() && session.isDestroyed()))
? session.generateInfoForCaller(true /* includeIcon */, callingUid)
: null;
}
return shouldFilterSession(mPm.snapshotComputer(), callingUid, result) ? null : result;
}
@Override
public ParceledListSlice<SessionInfo> getStagedSessions() {
final int callingUid = Binder.getCallingUid();
final List<SessionInfo> result = new ArrayList<>();
synchronized (mSessions) {
for (int i = 0; i < mSessions.size(); i++) {
final PackageInstallerSession session = mSessions.valueAt(i);
if (session.isStaged() && !session.isDestroyed()) {
result.add(session.generateInfoForCaller(false /* includeIcon */, callingUid));
}
}
}
final Computer snapshot = mPm.snapshotComputer();
result.removeIf(info -> shouldFilterSession(snapshot, callingUid, info));
return new ParceledListSlice<>(result);
}
@Override
public ParceledListSlice<SessionInfo> getAllSessions(int userId) {
final int callingUid = Binder.getCallingUid();
final Computer snapshot = mPm.snapshotComputer();
snapshot.enforceCrossUserPermission(callingUid, userId, true, false, "getAllSessions");
final List<SessionInfo> result = new ArrayList<>();
synchronized (mSessions) {
for (int i = 0; i < mSessions.size(); i++) {
final PackageInstallerSession session = mSessions.valueAt(i);
if (session.userId == userId && !session.hasParentSessionId()
&& !(session.isStaged() && session.isDestroyed())) {
result.add(session.generateInfoForCaller(false /* includeIcon */, callingUid));
}
}
}
result.removeIf(info -> shouldFilterSession(snapshot, callingUid, info));
return new ParceledListSlice<>(result);
}
@Override
public ParceledListSlice<SessionInfo> getMySessions(String installerPackageName, int userId) {
final Computer snapshot = mPm.snapshotComputer();
final int callingUid = Binder.getCallingUid();
snapshot.enforceCrossUserPermission(callingUid, userId, true, false, "getMySessions");
mAppOps.checkPackage(callingUid, installerPackageName);
final List<SessionInfo> result = new ArrayList<>();
synchronized (mSessions) {
for (int i = 0; i < mSessions.size(); i++) {
final PackageInstallerSession session = mSessions.valueAt(i);
SessionInfo info =
session.generateInfoForCaller(false /*withIcon*/, SYSTEM_UID);
if (Objects.equals(info.getInstallerPackageName(), installerPackageName)
&& session.userId == userId && !session.hasParentSessionId()
&& isCallingUidOwner(session)
&& (session.getInstallFlags() & PackageManager.INSTALL_UNARCHIVE_DRAFT)
== 0) {
result.add(info);
}
}
}
return new ParceledListSlice<>(result);
}
ParceledListSlice<SessionInfo> getHistoricalSessions(int userId) {
final int callingUid = Binder.getCallingUid();
final Computer snapshot = mPm.snapshotComputer();
snapshot.enforceCrossUserPermission(callingUid, userId, true, false, "getAllSessions");
final List<SessionInfo> result = new ArrayList<>();
synchronized (mSessions) {
for (int i = 0; i < mHistoricalSessions.size(); i++) {
final PackageInstallerHistoricalSession session = mHistoricalSessions.get(i);
if (userId == UserHandle.USER_ALL || session.userId == userId) {
result.add(session.generateInfo());
}
}
}
result.removeIf(info -> shouldFilterSession(snapshot, callingUid, info));
return new ParceledListSlice<>(result);
}
@Override
public void uninstall(VersionedPackage versionedPackage, String callerPackageName, int flags,
IntentSender statusReceiver, int userId) {
uninstall(
versionedPackage,
callerPackageName,
flags,
statusReceiver,
userId,
Binder.getCallingUid());
}
void uninstall(VersionedPackage versionedPackage, String callerPackageName, int flags,
IntentSender statusReceiver, int userId, int callingUid) {
final Computer snapshot = mPm.snapshotComputer();
snapshot.enforceCrossUserPermission(callingUid, userId, true, true, "uninstall");
if (!PackageManagerServiceUtils.isRootOrShell(callingUid)) {
mAppOps.checkPackage(callingUid, callerPackageName);
}
// Check whether the caller is device owner or affiliated profile owner, in which case we do
// it silently.
DevicePolicyManagerInternal dpmi =
LocalServices.getService(DevicePolicyManagerInternal.class);
final boolean canSilentlyInstallPackage =
dpmi != null && dpmi.canSilentlyInstallPackage(callerPackageName, callingUid);
final PackageDeleteObserverAdapter adapter = new PackageDeleteObserverAdapter(mContext,
statusReceiver, versionedPackage.getPackageName(),
canSilentlyInstallPackage, userId);
final boolean shouldShowConfirmationDialog =
(flags & PackageManager.DELETE_SHOW_DIALOG) != 0;
if (!shouldShowConfirmationDialog
&& mContext.checkCallingOrSelfPermission(Manifest.permission.DELETE_PACKAGES)
== PackageManager.PERMISSION_GRANTED) {
// Sweet, call straight through!
mPm.deletePackageVersioned(versionedPackage, adapter.getBinder(), userId, flags);
} else if (!shouldShowConfirmationDialog && canSilentlyInstallPackage) {
// Allow the device owner and affiliated profile owner to silently delete packages
// Need to clear the calling identity to get DELETE_PACKAGES permission
final long ident = Binder.clearCallingIdentity();
try {
mPm.deletePackageVersioned(versionedPackage, adapter.getBinder(), userId, flags);
} finally {
Binder.restoreCallingIdentity(ident);
}
DevicePolicyEventLogger
.createEvent(DevicePolicyEnums.UNINSTALL_PACKAGE)
.setAdmin(callerPackageName)
.write();
} else {
ApplicationInfo appInfo = snapshot.getApplicationInfo(callerPackageName, 0, userId);
if (appInfo.targetSdkVersion >= Build.VERSION_CODES.P) {
mContext.enforceCallingOrSelfPermission(Manifest.permission.REQUEST_DELETE_PACKAGES,
null);
}
// Take a short detour to confirm with user
final Intent intent = new Intent(Intent.ACTION_UNINSTALL_PACKAGE);
intent.setData(Uri.fromParts("package", versionedPackage.getPackageName(), null));
intent.putExtra(PackageInstaller.EXTRA_CALLBACK,
new PackageManager.UninstallCompleteCallback(adapter.getBinder().asBinder()));
if ((flags & PackageManager.DELETE_ARCHIVE) != 0) {
// Delete flags are passed to the uninstaller activity so it can be preserved
// in the follow-up uninstall operation after the user confirmation
intent.putExtra(PackageInstaller.EXTRA_DELETE_FLAGS, flags);
}
adapter.onUserActionRequired(intent);
}
}
@Override
public void uninstallExistingPackage(VersionedPackage versionedPackage,
String callerPackageName, IntentSender statusReceiver, int userId) {
final int callingUid = Binder.getCallingUid();
mContext.enforceCallingOrSelfPermission(Manifest.permission.DELETE_PACKAGES, null);
final Computer snapshot = mPm.snapshotComputer();
snapshot.enforceCrossUserPermission(callingUid, userId, true, true, "uninstall");
if (!PackageManagerServiceUtils.isRootOrShell(callingUid)) {
mAppOps.checkPackage(callingUid, callerPackageName);
}
final PackageDeleteObserverAdapter adapter = new PackageDeleteObserverAdapter(mContext,
statusReceiver, versionedPackage.getPackageName(), false, userId);
mPm.deleteExistingPackageAsUser(versionedPackage, adapter.getBinder(), userId);
}
@Override
public void installExistingPackage(String packageName, int installFlags, int installReason,
IntentSender statusReceiver, int userId, List<String> allowListedPermissions) {
var result = mPm.installExistingPackageAsUser(packageName, userId,
installFlags, installReason, allowListedPermissions, statusReceiver);
int returnCode = result.first;
IntentSender onCompleteSender = result.second;
if (onCompleteSender != null) {
InstallPackageHelper.onInstallComplete(returnCode, mContext, onCompleteSender);
}
}
@android.annotation.EnforcePermission(android.Manifest.permission.INSTALL_PACKAGES)
@Override
public void setPermissionsResult(int sessionId, boolean accepted) {
setPermissionsResult_enforcePermission();
synchronized (mSessions) {
PackageInstallerSession session = mSessions.get(sessionId);
if (session != null) {
session.setPermissionsResult(accepted);
}
}
}
private boolean isValidForInstallConstraints(PackageStateInternal ps,
String installerPackageName, int installerUid, String packageName) {
final var snapshot = mPm.snapshotComputer();
final var isSelfUpdatePermissionGranted =
(snapshot.checkUidPermission(android.Manifest.permission.INSTALL_SELF_UPDATES,
installerUid) == PackageManager.PERMISSION_GRANTED);
final var isSelfUpdateAllowed = isSelfUpdatePermissionGranted && TextUtils.equals(
packageName, installerPackageName);
return TextUtils.equals(ps.getInstallSource().mInstallerPackageName, installerPackageName)
|| TextUtils.equals(ps.getInstallSource().mUpdateOwnerPackageName,
installerPackageName) || isSelfUpdateAllowed;
}
private CompletableFuture<InstallConstraintsResult> checkInstallConstraintsInternal(
String installerPackageName, List<String> packageNames,
InstallConstraints constraints, long timeoutMillis) {
Objects.requireNonNull(packageNames);
Objects.requireNonNull(constraints);
final var snapshot = mPm.snapshotComputer();
final int callingUid = Binder.getCallingUid();
final var callingPackageName = snapshot.getNameForUid(callingUid);
if (!TextUtils.equals(callingPackageName, installerPackageName)) {
throw new SecurityException("The installerPackageName set by the caller doesn't match "
+ "the caller's own package name.");
}
if (!PackageManagerServiceUtils.isSystemOrRootOrShell(callingUid)) {
for (var packageName : packageNames) {
var ps = snapshot.getPackageStateInternal(packageName);
if (ps == null || !isValidForInstallConstraints(ps, installerPackageName,
callingUid, packageName)) {
throw new SecurityException("Caller has no access to package " + packageName);
}
}
}
return mGentleUpdateHelper.checkInstallConstraints(
packageNames, constraints, timeoutMillis);
}
@Override
public void checkInstallConstraints(String installerPackageName, List<String> packageNames,
InstallConstraints constraints, RemoteCallback callback) {
Objects.requireNonNull(callback);
var future = checkInstallConstraintsInternal(
installerPackageName, packageNames, constraints, /*timeoutMillis=*/0);
future.thenAccept(result -> {
var b = new Bundle();
b.putParcelable("result", result);
callback.sendResult(b);
});
}
@Override
public void waitForInstallConstraints(String installerPackageName, List<String> packageNames,
InstallConstraints constraints, IntentSender callback, long timeoutMillis) {
Objects.requireNonNull(callback);
if (timeoutMillis < 0 || timeoutMillis > MAX_INSTALL_CONSTRAINTS_TIMEOUT_MILLIS) {
throw new IllegalArgumentException("Invalid timeoutMillis=" + timeoutMillis);
}
var future = checkInstallConstraintsInternal(
installerPackageName, packageNames, constraints, timeoutMillis);
future.thenAccept(result -> {
final var intent = new Intent();
intent.putExtra(Intent.EXTRA_PACKAGES, packageNames.toArray(new String[0]));
intent.putExtra(PackageInstaller.EXTRA_INSTALL_CONSTRAINTS, constraints);
intent.putExtra(PackageInstaller.EXTRA_INSTALL_CONSTRAINTS_RESULT, result);
try {
final BroadcastOptions options = BroadcastOptions.makeBasic();
options.setPendingIntentBackgroundActivityLaunchAllowed(false);
callback.sendIntent(mContext, 0, intent, null /* onFinished*/,
null /* handler */, null /* requiredPermission */, options.toBundle());
} catch (SendIntentException ignore) {
}
});
}
@Override
public void registerCallback(IPackageInstallerCallback callback, int userId) {
final Computer snapshot = mPm.snapshotComputer();
snapshot.enforceCrossUserPermission(Binder.getCallingUid(), userId, true, false,
"registerCallback");
registerCallback(callback, eventUserId -> userId == eventUserId);
}
/**
* Assume permissions already checked and caller's identity cleared
*/
public void registerCallback(IPackageInstallerCallback callback, IntPredicate userCheck) {
mCallbacks.register(callback, new BroadcastCookie(Binder.getCallingUid(), userCheck));
}
@Override
public void unregisterCallback(IPackageInstallerCallback callback) {
mCallbacks.unregister(callback);
}
@Override
public PackageInstallerSession getSession(int sessionId) {
synchronized (mSessions) {
return mSessions.get(sessionId);
}
}
@Override
public PackageSessionVerifier getSessionVerifier() {
return mSessionVerifier;
}
@Override
public GentleUpdateHelper getGentleUpdateHelper() {
return mGentleUpdateHelper;
}
@Override
public void bypassNextStagedInstallerCheck(boolean value) {
if (!PackageManagerServiceUtils.isSystemOrRootOrShell(Binder.getCallingUid())) {
throw new SecurityException("Caller not allowed to bypass staged installer check");
}
mBypassNextStagedInstallerCheck = value;
}
@Override
public void bypassNextAllowedApexUpdateCheck(boolean value) {
if (!PackageManagerServiceUtils.isSystemOrRootOrShell(Binder.getCallingUid())) {
throw new SecurityException("Caller not allowed to bypass allowed apex update check");
}
mBypassNextAllowedApexUpdateCheck = value;
}
@Override
public void disableVerificationForUid(int uid) {
if (!PackageManagerServiceUtils.isSystemOrRootOrShell(Binder.getCallingUid())) {
throw new SecurityException("Operation not allowed for caller");
}
mDisableVerificationForUid = uid;
}
/**
* Set an installer to allow for the unlimited silent updates.
*/
@Override
public void setAllowUnlimitedSilentUpdates(@Nullable String installerPackageName) {
if (!PackageManagerServiceUtils.isSystemOrRootOrShell(Binder.getCallingUid())) {
throw new SecurityException("Caller not allowed to unlimite silent updates");
}
mSilentUpdatePolicy.setAllowUnlimitedSilentUpdates(installerPackageName);
}
/**
* Set the silent updates throttle time in seconds.
*/
@Override
public void setSilentUpdatesThrottleTime(long throttleTimeInSeconds) {
if (!PackageManagerServiceUtils.isSystemOrRootOrShell(Binder.getCallingUid())) {
throw new SecurityException("Caller not allowed to set silent updates throttle time");
}
mSilentUpdatePolicy.setSilentUpdatesThrottleTime(throttleTimeInSeconds);
}
@Override
public void requestArchive(
@NonNull String packageName,
@NonNull String callerPackageName,
@NonNull IntentSender intentSender,
@NonNull UserHandle userHandle,
@DeleteFlags int flags) {
mPackageArchiver.requestArchive(packageName, callerPackageName, intentSender,
userHandle, flags);
}
@Override
public void requestUnarchive(
@NonNull String packageName,
@NonNull String callerPackageName,
@NonNull IntentSender statusReceiver,
@NonNull UserHandle userHandle) {
mPackageArchiver.requestUnarchive(packageName, callerPackageName, statusReceiver,
userHandle);
}
@Override
public void installPackageArchived(
@NonNull ArchivedPackageParcel archivedPackageParcel,
@NonNull SessionParams params,
@NonNull IntentSender statusReceiver,
@NonNull String installerPackageName,
@NonNull UserHandle userHandle) {
Objects.requireNonNull(params);
Objects.requireNonNull(archivedPackageParcel);
Objects.requireNonNull(statusReceiver);
Objects.requireNonNull(installerPackageName);
Objects.requireNonNull(userHandle);
final int callingUid = Binder.getCallingUid();
final int userId = userHandle.getIdentifier();
final Computer snapshot = mPm.snapshotComputer();
snapshot.enforceCrossUserPermission(callingUid, userId, true, true,
"installPackageArchived");
if (mContext.checkCallingOrSelfPermission(Manifest.permission.INSTALL_PACKAGES)
!= PackageManager.PERMISSION_GRANTED) {
throw new SecurityException("You need the "
+ "com.android.permission.INSTALL_PACKAGES permission "
+ "to request archived package install");
}
params.installFlags |= PackageManager.INSTALL_ARCHIVED;
if (params.dataLoaderParams != null) {
throw new IllegalArgumentException(
"Incompatible session param: dataLoaderParams has to be null");
}
params.setDataLoaderParams(
PackageManagerShellCommandDataLoader.getStreamingDataLoaderParams(null));
var metadata = PackageManagerShellCommandDataLoader.Metadata.forArchived(
archivedPackageParcel);
// Create and commit install archived session.
// Session belongs to the system_server and would not appear anywhere in the Public APIs.
Binder.withCleanCallingIdentity(() -> {
PackageInstallerSession session = null;
try {
var sessionId = createSessionInternal(params, installerPackageName, null
/*installerAttributionTag*/, Binder.getCallingUid(), userId);
session = openSessionInternal(sessionId);
session.addFile(LOCATION_DATA_APP, "base", 0 /*lengthBytes*/,
metadata.toByteArray(), null /*signature*/);
session.commit(statusReceiver, false /*forTransfer*/);
} catch (IOException e) {
throw ExceptionUtils.wrap(e);
} finally {
if (session != null) {
session.close();
}
}
});
}
@Override
public void reportUnarchivalStatus(
int unarchiveId,
@UnarchivalStatus int status,
long requiredStorageBytes,
@Nullable PendingIntent userActionIntent,
@NonNull UserHandle userHandle) {
verifyReportUnarchiveStatusInput(
status, requiredStorageBytes, userActionIntent, userHandle);
int userId = userHandle.getIdentifier();
int binderUid = Binder.getCallingUid();
synchronized (mSessions) {
PackageInstallerSession session = mSessions.get(unarchiveId);
if (session == null || session.userId != userId
|| session.params.appPackageName == null) {
throw new ParcelableException(new PackageManager.NameNotFoundException(
TextUtils.formatSimple(
"No valid session with unarchival ID %s found for user %s.",
unarchiveId, userId)));
}
if (!isCallingUidOwner(session)) {
throw new SecurityException(TextUtils.formatSimple(
"The caller UID %s does not have access to the session with unarchiveId "
+ "%d.",
binderUid, unarchiveId));
}
IntentSender unarchiveIntentSender = session.params.unarchiveIntentSender;
if (unarchiveIntentSender == null) {
throw new IllegalStateException(
TextUtils.formatSimple(
"Unarchival status for ID %s has already been set or a "
+ "session has been created for it already by the "
+ "caller.",
unarchiveId));
}
// Execute expensive calls outside the sync block.
mPm.mHandler.post(
() -> mPackageArchiver.notifyUnarchivalListener(status,
session.getInstallerPackageName(),
session.params.appPackageName, requiredStorageBytes, userActionIntent,
unarchiveIntentSender, userId));
session.params.unarchiveIntentSender = null;
if (status != UNARCHIVAL_OK) {
Binder.withCleanCallingIdentity(session::abandon);
}
}
}
private static void verifyReportUnarchiveStatusInput(int status, long requiredStorageBytes,
@Nullable PendingIntent userActionIntent,
@NonNull UserHandle userHandle) {
Objects.requireNonNull(userHandle);
if (status == UNARCHIVAL_ERROR_USER_ACTION_NEEDED) {
Objects.requireNonNull(userActionIntent);
}
if (status == UNARCHIVAL_ERROR_INSUFFICIENT_STORAGE && requiredStorageBytes <= 0) {
throw new IllegalStateException(
"Insufficient storage error set, but requiredStorageBytes unspecified.");
}
if (status != UNARCHIVAL_ERROR_INSUFFICIENT_STORAGE && requiredStorageBytes > 0) {
throw new IllegalStateException(
TextUtils.formatSimple("requiredStorageBytes set, but error is %s.", status)
);
}
if (!List.of(
UNARCHIVAL_OK,
UNARCHIVAL_ERROR_USER_ACTION_NEEDED,
UNARCHIVAL_ERROR_INSUFFICIENT_STORAGE,
UNARCHIVAL_ERROR_NO_CONNECTIVITY,
UNARCHIVAL_ERROR_INSTALLER_DISABLED,
UNARCHIVAL_ERROR_INSTALLER_UNINSTALLED,
UNARCHIVAL_GENERIC_ERROR).contains(status)) {
throw new IllegalStateException("Invalid status code passed " + status);
}
}
private static int getSessionCount(SparseArray<PackageInstallerSession> sessions,
int installerUid) {
int count = 0;
final int size = sessions.size();
for (int i = 0; i < size; i++) {
final PackageInstallerSession session = sessions.valueAt(i);
if (session.getInstallerUid() == installerUid) {
count++;
}
}
return count;
}
private boolean isCallingUidOwner(PackageInstallerSession session) {
final int callingUid = Binder.getCallingUid();
if (callingUid == Process.ROOT_UID) {
return true;
} else {
return (session != null) && (callingUid == session.getInstallerUid());
}
}
private boolean shouldFilterSession(@NonNull Computer snapshot, int uid, int sessionId) {
final PackageInstallerSession session = getSession(sessionId);
if (session == null) {
return false;
}
return uid != session.getInstallerUid()
&& !snapshot.canQueryPackage(uid, session.getPackageName());
}
static class PackageDeleteObserverAdapter extends PackageDeleteObserver {
private final Context mContext;
private final IntentSender mTarget;
private final String mPackageName;
private final Notification mNotification;
public PackageDeleteObserverAdapter(Context context, IntentSender target,
String packageName, boolean showNotification, int userId) {
mContext = context;
mTarget = target;
mPackageName = packageName;
if (showNotification) {
mNotification = buildSuccessNotification(mContext,
getDeviceOwnerDeletedPackageMsg(),
packageName,
userId);
} else {
mNotification = null;
}
}
private String getDeviceOwnerDeletedPackageMsg() {
final long ident = Binder.clearCallingIdentity();
try {
DevicePolicyManager dpm = mContext.getSystemService(DevicePolicyManager.class);
return dpm.getResources().getString(PACKAGE_DELETED_BY_DO,
() -> mContext.getString(R.string.package_deleted_device_owner));
} finally {
Binder.restoreCallingIdentity(ident);
}
}
@Override
public void onUserActionRequired(Intent intent) {
if (mTarget == null) {
return;
}
final Intent fillIn = new Intent();
fillIn.putExtra(PackageInstaller.EXTRA_PACKAGE_NAME, mPackageName);
fillIn.putExtra(PackageInstaller.EXTRA_STATUS,
PackageInstaller.STATUS_PENDING_USER_ACTION);
fillIn.putExtra(Intent.EXTRA_INTENT, intent);
try {
final BroadcastOptions options = BroadcastOptions.makeBasic();
options.setPendingIntentBackgroundActivityLaunchAllowed(false);
mTarget.sendIntent(mContext, 0, fillIn, null /* onFinished*/,
null /* handler */, null /* requiredPermission */, options.toBundle());
} catch (SendIntentException ignored) {
}
}
@Override
public void onPackageDeleted(String basePackageName, int returnCode, String msg) {
if (PackageManager.DELETE_SUCCEEDED == returnCode && mNotification != null) {
NotificationManager notificationManager = (NotificationManager)
mContext.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.notify(basePackageName,
SystemMessage.NOTE_PACKAGE_STATE,
mNotification);
}
if (mTarget == null) {
return;
}
final Intent fillIn = new Intent();
fillIn.putExtra(PackageInstaller.EXTRA_PACKAGE_NAME, mPackageName);
fillIn.putExtra(PackageInstaller.EXTRA_STATUS,
PackageManager.deleteStatusToPublicStatus(returnCode));
fillIn.putExtra(PackageInstaller.EXTRA_STATUS_MESSAGE,
PackageManager.deleteStatusToString(returnCode, msg));
fillIn.putExtra(PackageInstaller.EXTRA_LEGACY_STATUS, returnCode);
try {
final BroadcastOptions options = BroadcastOptions.makeBasic();
options.setPendingIntentBackgroundActivityLaunchAllowed(false);
mTarget.sendIntent(mContext, 0, fillIn, null /* onFinished*/,
null /* handler */, null /* requiredPermission */, options.toBundle());
} catch (SendIntentException ignored) {
}
}
}
/**
* Build a notification for package installation / deletion by device owners that is shown if
* the operation succeeds.
*/
static Notification buildSuccessNotification(Context context, String contentText,
String basePackageName, int userId) {
PackageInfo packageInfo = null;
try {
packageInfo = AppGlobals.getPackageManager().getPackageInfo(
basePackageName, PackageManager.MATCH_STATIC_SHARED_AND_SDK_LIBRARIES, userId);
} catch (RemoteException ignored) {
}
if (packageInfo == null || packageInfo.applicationInfo == null) {
Slog.w(TAG, "Notification not built for package: " + basePackageName);
return null;
}
PackageManager pm = context.getPackageManager();
Bitmap packageIcon = ImageUtils.buildScaledBitmap(
packageInfo.applicationInfo.loadIcon(pm),
context.getResources().getDimensionPixelSize(
android.R.dimen.notification_large_icon_width),
context.getResources().getDimensionPixelSize(
android.R.dimen.notification_large_icon_height));
CharSequence packageLabel = packageInfo.applicationInfo.loadLabel(pm);
return new Notification.Builder(context, SystemNotificationChannels.DEVICE_ADMIN)
.setSmallIcon(R.drawable.ic_check_circle_24px)
.setColor(context.getResources().getColor(
R.color.system_notification_accent_color))
.setContentTitle(packageLabel)
.setContentText(contentText)
.setStyle(new Notification.BigTextStyle().bigText(contentText))
.setLargeIcon(packageIcon)
.build();
}
public static <E> ArraySet<E> newArraySet(E... elements) {
final ArraySet<E> set = new ArraySet<E>();
if (elements != null) {
set.ensureCapacity(elements.length);
Collections.addAll(set, elements);
}
return set;
}
private static final class BroadcastCookie {
public final int callingUid;
public final IntPredicate userCheck;
BroadcastCookie(int callingUid, IntPredicate userCheck) {
this.callingUid = callingUid;
this.userCheck = userCheck;
}
}
private class Callbacks extends Handler {
private static final int MSG_SESSION_CREATED = 1;
private static final int MSG_SESSION_BADGING_CHANGED = 2;
private static final int MSG_SESSION_ACTIVE_CHANGED = 3;
private static final int MSG_SESSION_PROGRESS_CHANGED = 4;
private static final int MSG_SESSION_FINISHED = 5;
private final RemoteCallbackList<IPackageInstallerCallback>
mCallbacks = new RemoteCallbackList<>();
public Callbacks(Looper looper) {
super(looper);
}
public void register(IPackageInstallerCallback callback, BroadcastCookie cookie) {
mCallbacks.register(callback, cookie);
}
public void unregister(IPackageInstallerCallback callback) {
mCallbacks.unregister(callback);
}
@Override
public void handleMessage(Message msg) {
final int sessionId = msg.arg1;
final int userId = msg.arg2;
final int n = mCallbacks.beginBroadcast();
final Computer snapshot = mPm.snapshotComputer();
for (int i = 0; i < n; i++) {
final IPackageInstallerCallback callback = mCallbacks.getBroadcastItem(i);
final BroadcastCookie cookie = (BroadcastCookie) mCallbacks.getBroadcastCookie(i);
if (cookie.userCheck.test(userId)
&& !shouldFilterSession(snapshot, cookie.callingUid, sessionId)) {
try {
invokeCallback(callback, msg);
} catch (RemoteException ignored) {
}
}
}
mCallbacks.finishBroadcast();
}
private void invokeCallback(IPackageInstallerCallback callback, Message msg)
throws RemoteException {
final int sessionId = msg.arg1;
switch (msg.what) {
case MSG_SESSION_CREATED:
callback.onSessionCreated(sessionId);
break;
case MSG_SESSION_BADGING_CHANGED:
callback.onSessionBadgingChanged(sessionId);
break;
case MSG_SESSION_ACTIVE_CHANGED:
callback.onSessionActiveChanged(sessionId, (boolean) msg.obj);
break;
case MSG_SESSION_PROGRESS_CHANGED:
callback.onSessionProgressChanged(sessionId, (float) msg.obj);
break;
case MSG_SESSION_FINISHED:
callback.onSessionFinished(sessionId, (boolean) msg.obj);
break;
}
}
private void notifySessionCreated(int sessionId, int userId) {
obtainMessage(MSG_SESSION_CREATED, sessionId, userId).sendToTarget();
}
private void notifySessionBadgingChanged(int sessionId, int userId) {
obtainMessage(MSG_SESSION_BADGING_CHANGED, sessionId, userId).sendToTarget();
}
private void notifySessionActiveChanged(int sessionId, int userId, boolean active) {
obtainMessage(MSG_SESSION_ACTIVE_CHANGED, sessionId, userId, active).sendToTarget();
}
private void notifySessionProgressChanged(int sessionId, int userId, float progress) {
obtainMessage(MSG_SESSION_PROGRESS_CHANGED, sessionId, userId, progress).sendToTarget();
}
public void notifySessionFinished(int sessionId, int userId, boolean success) {
obtainMessage(MSG_SESSION_FINISHED, sessionId, userId, success).sendToTarget();
}
}
static class ParentChildSessionMap {
private final TreeMap<PackageInstallerSession, TreeSet<PackageInstallerSession>>
mSessionMap;
private final Comparator<PackageInstallerSession> mSessionCreationComparator =
Comparator.comparingLong(
(PackageInstallerSession sess) -> sess != null ? sess.createdMillis : -1)
.thenComparingInt(sess -> sess != null ? sess.sessionId : -1);
ParentChildSessionMap() {
mSessionMap = new TreeMap<>(mSessionCreationComparator);
}
boolean containsSession() {
return !(mSessionMap.isEmpty());
}
private void addParentSession(PackageInstallerSession session) {
if (!mSessionMap.containsKey(session)) {
mSessionMap.put(session, new TreeSet<>(mSessionCreationComparator));
}
}
private void addChildSession(PackageInstallerSession session,
PackageInstallerSession parentSession) {
addParentSession(parentSession);
mSessionMap.get(parentSession).add(session);
}
void addSession(PackageInstallerSession session,
PackageInstallerSession parentSession) {
if (session.hasParentSessionId()) {
addChildSession(session, parentSession);
} else {
addParentSession(session);
}
}
void dump(String tag, IndentingPrintWriter pw) {
pw.println(tag + " install sessions:");
pw.increaseIndent();
for (Map.Entry<PackageInstallerSession, TreeSet<PackageInstallerSession>> entry
: mSessionMap.entrySet()) {
PackageInstallerSession parentSession = entry.getKey();
if (parentSession != null) {
pw.print(tag + " ");
parentSession.dump(pw);
pw.println();
pw.increaseIndent();
}
for (PackageInstallerSession childSession : entry.getValue()) {
pw.print(tag + " Child ");
childSession.dump(pw);
pw.println();
}
pw.decreaseIndent();
}
pw.println();
pw.decreaseIndent();
}
}
void dump(IndentingPrintWriter pw) {
synchronized (mSessions) {
ParentChildSessionMap activeSessionMap = new ParentChildSessionMap();
ParentChildSessionMap orphanedChildSessionMap = new ParentChildSessionMap();
ParentChildSessionMap finalizedSessionMap = new ParentChildSessionMap();
int N = mSessions.size();
for (int i = 0; i < N; i++) {
final PackageInstallerSession session = mSessions.valueAt(i);
final PackageInstallerSession rootSession = session.hasParentSessionId()
? getSession(session.getParentSessionId())
: session;
// Do not print orphaned child sessions as active install sessions
if (rootSession == null) {
orphanedChildSessionMap.addSession(session, rootSession);
continue;
}
// Do not print finalized staged session as active install sessions
if (rootSession.isStagedAndInTerminalState()) {
finalizedSessionMap.addSession(session, rootSession);
continue;
}
activeSessionMap.addSession(session, rootSession);
}
activeSessionMap.dump("Active", pw);
if (orphanedChildSessionMap.containsSession()) {
// Presence of orphaned sessions indicate leak in cleanup for multi-package and
// should be cleaned up.
orphanedChildSessionMap.dump("Orphaned", pw);
}
finalizedSessionMap.dump("Finalized", pw);
pw.println("Historical install sessions:");
pw.increaseIndent();
N = mHistoricalSessions.size();
for (int i = 0; i < N; i++) {
mHistoricalSessions.get(i).dump(pw);
pw.println();
}
pw.println();
pw.decreaseIndent();
pw.println("Legacy install sessions:");
pw.increaseIndent();
pw.println(mLegacySessions.toString());
pw.println();
pw.decreaseIndent();
}
mSilentUpdatePolicy.dump(pw);
mGentleUpdateHelper.dump(pw);
}
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
public class InternalCallback {
public void onSessionBadgingChanged(PackageInstallerSession session) {
mCallbacks.notifySessionBadgingChanged(session.sessionId, session.userId);
mSettingsWriteRequest.schedule();
}
public void onSessionActiveChanged(PackageInstallerSession session, boolean active) {
mCallbacks.notifySessionActiveChanged(session.sessionId, session.userId,
active);
}
public void onSessionProgressChanged(PackageInstallerSession session, float progress) {
mCallbacks.notifySessionProgressChanged(session.sessionId, session.userId,
progress);
}
public void onSessionChanged(PackageInstallerSession session) {
session.markUpdated();
mSettingsWriteRequest.schedule();
// TODO(b/210359798): Remove the session.isStaged() check. Some apps assume this
// broadcast is sent by only staged sessions and call isStagedSessionApplied() without
// checking if it is a staged session or not and cause exception.
if (mOkToSendBroadcasts && !session.isDestroyed() && session.isStaged()) {
// we don't scrub the data here as this is sent only to the installer several
// privileged system packages
sendSessionUpdatedBroadcast(
session.generateInfoForCaller(false/*icon*/, SYSTEM_UID),
session.userId);
}
}
public void onSessionFinished(final PackageInstallerSession session, boolean success) {
if (success) {
// There is a timing issue here, if the callback opens the session again in
// notifySessionFinished() immediately, the session may not be removed from
// the mSession. But to avoid adding unknown latency, only notifying failures
// are moved to the last of posted runnable, notifying success cases are
// still kept here.
mCallbacks.notifySessionFinished(session.sessionId, session.userId, success);
}
mInstallHandler.post(new Runnable() {
@Override
public void run() {
if (session.isStaged() && !success) {
mStagingManager.abortSession(session.mStagedSession);
}
synchronized (mSessions) {
// Child sessions will be removed along with its parent as a whole
if (!session.hasParentSessionId()) {
// Retain policy:
// 1. Don't keep non-staged sessions
// 2. Don't keep explicitly abandoned sessions
// 3. Don't keep sessions that fail validation (isCommitted() is false)
boolean shouldRemove = !session.isStaged() || session.isDestroyed()
|| !session.isCommitted();
if (shouldRemove) {
removeActiveSession(session);
}
}
final File appIconFile = buildAppIconFile(session.sessionId);
if (appIconFile.exists()) {
appIconFile.delete();
}
mSettingsWriteRequest.runNow();
}
if (!success) {
mCallbacks.notifySessionFinished(
session.sessionId, session.userId, success);
}
}
});
}
public void onSessionPrepared(PackageInstallerSession session) {
// We prepared the destination to write into; we want to persist
// this, but it's not critical enough to block for.
mSettingsWriteRequest.schedule();
}
public void onSessionSealedBlocking(PackageInstallerSession session) {
// It's very important that we block until we've recorded the
// session as being sealed, since we never want to allow mutation
// after sealing.
mSettingsWriteRequest.runNow();
}
}
/**
* Send a {@code PackageInstaller.ACTION_SESSION_UPDATED} broadcast intent, containing
* the {@code sessionInfo} in the extra field {@code PackageInstaller.EXTRA_SESSION}.
*/
private void sendSessionUpdatedBroadcast(PackageInstaller.SessionInfo sessionInfo,
int userId) {
if (TextUtils.isEmpty(sessionInfo.installerPackageName)) {
return;
}
Intent sessionUpdatedIntent = new Intent(PackageInstaller.ACTION_SESSION_UPDATED)
.putExtra(PackageInstaller.EXTRA_SESSION, sessionInfo)
.setPackage(sessionInfo.installerPackageName);
mContext.sendBroadcastAsUser(sessionUpdatedIntent, UserHandle.of(userId));
}
/**
* Abandon unfinished sessions if the installer package has been uninstalled.
* @param installerAppId the app ID of the installer package that has been uninstalled.
* @param userId the user that has the installer package uninstalled.
*/
void onInstallerPackageDeleted(int installerAppId, int userId) {
synchronized (mSessions) {
for (int i = 0; i < mSessions.size(); i++) {
final PackageInstallerSession session = mSessions.valueAt(i);
if (!matchesInstaller(session, installerAppId, userId)) {
continue;
}
// Find parent session and only abandon parent session if installer matches
PackageInstallerSession root = !session.hasParentSessionId()
? session : mSessions.get(session.getParentSessionId());
if (root != null && matchesInstaller(root, installerAppId, userId)
&& !root.isDestroyed()) {
root.abandon();
}
}
}
}
private boolean matchesInstaller(PackageInstallerSession session, int installerAppId,
int userId) {
final int installerUid = session.getInstallerUid();
if (installerAppId == UserHandle.USER_ALL) {
return UserHandle.getAppId(installerUid) == installerAppId;
} else {
return UserHandle.getUid(userId, installerAppId) == installerUid;
}
}
}