/*
 * 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.DEVELOPER_VERIFICATION_POLICY_NONE;
import static android.content.pm.PackageInstaller.LOCATION_DATA_APP;
import static android.content.pm.PackageInstaller.SessionParams.MAX_PERMISSION_STATES_SIZE;
import static android.content.pm.PackageInstaller.SessionParams.MAX_URI_LENGTH;
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.DELETE_ARCHIVE;
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.PackageArchiver.isArchivingEnabled;
import static com.android.server.pm.PackageInstallerSession.isValidVerificationPolicy;
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.EnforcePermission;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
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.app.role.RoleManager;
import android.content.ComponentName;
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.DeveloperVerificationUserConfirmationInfo;
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.PermissionEnforcer;
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.pm.parsing.PackageParser2;
import com.android.internal.util.ArrayUtils;
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.pkg.PackageStateInternal;
import com.android.server.pm.utils.RequestThrottle;
import com.android.server.pm.verify.developer.DeveloperVerifierController;

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";
    private static final String TAG_DEVELOPER_VERIFICATION_POLICY_PER_USER =
            "developerVerificationPolicyPerUser";
    private static final String ATTR_USER_ID = "userId";
    private static final String ATTR_DEVELOPER_VERIFICATION_POLICY = "developerVerificationPolicy";

    /** 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,
            Manifest.permission.POST_PROMOTED_NOTIFICATIONS
    );

    private static final String ROLE_SYSTEM_APP_PROTECTION_SERVICE =
            "android.app.role.SYSTEM_APP_PROTECTION_SERVICE";

    final PackageArchiver mPackageArchiver;
    private final Context mContext;
    private final PackageManagerService mPm;
    private final ApexManager mApexManager;
    private final StagingManager mStagingManager;

    private AppOpsManager mAppOps;
    @NonNull
    private final DeveloperVerifierController mDeveloperVerifierController;
    private final InstallDependencyHelper mInstallDependencyHelper;

    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);
        }
    };

    /**
     * Default verification policy for incoming installation sessions, mapped from userId to policy.
     */
    @GuardedBy("mDeveloperVerificationPolicyPerUser")
    private final SparseIntArray mDeveloperVerificationPolicyPerUser = new SparseIntArray(1);
    /**
     * Default developer verification policy for a new user.
     */
    private static final int DEFAULT_VERIFICATION_POLICY = DEVELOPER_VERIFICATION_POLICY_NONE;

    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(),
            () -> {
                return writeSessions();
            });

    public PackageInstallerService(Context context, PackageManagerService pm,
            Supplier<PackageParser2> apexParserSupplier,
            @Nullable ComponentName developerVerificationServiceProvider) {
        super(PermissionEnforcer.fromContext(context));

        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);
        mDeveloperVerifierController = DeveloperVerifierController.getInstance(context,
                mInstallHandler, developerVerificationServiceProvider);
        mInstallDependencyHelper = new InstallDependencyHelper(mContext,
                mPm.mInjector.getSharedLibrariesImpl(), this);

        LocalServices.getService(SystemServiceManager.class).startService(
                new Lifecycle(context, this));
    }

    StagingManager getStagingManager() {
        return mStagingManager;
    }

    InstallDependencyHelper getInstallDependencyHelper() {
        return mInstallDependencyHelper;
    }

    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();
            }
        }

        // Clean up the per-user developer verification policies in case some previous users have
        // been removed since the last reboot.
        final int[] users = mPm.mUserManager.getUserIds();
        synchronized (mDeveloperVerificationPolicyPerUser) {
            int size = mDeveloperVerificationPolicyPerUser.size();
            for (int i = size - 1; i >= 0; i--) {
                if (!ArrayUtils.contains(users, mDeveloperVerificationPolicyPerUser.keyAt(i))) {
                    mDeveloperVerificationPolicyPerUser.removeAt(i);
                }
            }
            // If the per-user developer verification policy was never set before for any user,
            // add a default policy for the user.
            for (int i = 0; i < users.length; i++) {
                if (mDeveloperVerificationPolicyPerUser.indexOfKey(users[i]) < 0) {
                    mDeveloperVerificationPolicyPerUser.put(users[i], DEFAULT_VERIFICATION_POLICY);
                }
            }
        }

        // 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,
                                    mDeveloperVerifierController, mInstallDependencyHelper);
                        } catch (Exception e) {
                            Slog.e(TAG, "Could not read session", e);
                            continue;
                        }
                        mSessions.put(session.sessionId, session);
                        mAllocatedSessions.put(session.sessionId, true);
                    } else if (TAG_DEVELOPER_VERIFICATION_POLICY_PER_USER.equals(tag)) {
                        int userId = in.getAttributeInt(null, ATTR_USER_ID, -1);
                        if (userId == -1) {
                            continue;
                        }
                        int policy =
                                in.getAttributeInt(null, ATTR_DEVELOPER_VERIFICATION_POLICY, -1);
                        if (policy == -1) {
                            continue;
                        }
                        synchronized (mDeveloperVerificationPolicyPerUser) {
                            mDeveloperVerificationPolicyPerUser.put(userId, policy);
                        }
                    }
                }
            }
        } 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, /* isSessionExpired= */ true);
            }
        }
    }

    /**
     * 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, boolean isSessionExpired) {
        mSessions.remove(session.sessionId);
        addHistoricalSessionLocked(session);
        if (isSessionExpired) {
            session.onSessionExpired();
        }
        for (PackageInstallerSession child : session.getChildSessions()) {
            mSessions.remove(child.sessionId);
            addHistoricalSessionLocked(child);
            if (isSessionExpired) {
                child.onSessionExpired();
            }
        }
    }

    @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);
    }

    private boolean writeSessions() {
        if (LOGD) Slog.v(TAG, "writeSessions()");
        final PackageInstallerSession[] sessions;
        synchronized (mSessions) {
            final int size = mSessions.size();
            sessions = new PackageInstallerSession[size];
            for (int i = 0; i < size; i++) {
                sessions[i] = mSessions.valueAt(i);
            }
        }

        FileOutputStream fos = null;
        try {
            fos = mSessionsFile.startWrite();

            final TypedXmlSerializer out = Xml.resolveSerializer(fos);
            out.startDocument(null, true);
            out.startTag(null, TAG_SESSIONS);
            for (var session : sessions) {
                session.write(out, mSessionsDir);
            }
            out.endTag(null, TAG_SESSIONS);

            // Preserve current developer verification policy per user to disk.
            synchronized (mDeveloperVerificationPolicyPerUser) {
                for (int i = 0; i < mDeveloperVerificationPolicyPerUser.size(); i++) {
                    out.startTag(null, TAG_DEVELOPER_VERIFICATION_POLICY_PER_USER);
                    out.attributeInt(null, ATTR_USER_ID,
                            mDeveloperVerificationPolicyPerUser.keyAt(i));
                    out.attributeInt(null, ATTR_DEVELOPER_VERIFICATION_POLICY,
                            mDeveloperVerificationPolicyPerUser.valueAt(i));
                    out.endTag(null, TAG_DEVELOPER_VERIFICATION_POLICY_PER_USER);
                }
            }
            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 requested installer package name.
        if (params.installerPackageName != null && !isValidPackageName(
                params.installerPackageName)) {
            params.installerPackageName = null;
        }

        // Validate installer package name.
        if (installerPackageName != null && !isValidPackageName(installerPackageName)) {
            installerPackageName = null;
        }

        String 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 (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");
            }
            if (Build.IS_USER) {
                throw new SecurityException("Not allowed to perform APEX updates on user builds");
            }
        } 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
                && !PackageManagerServiceUtils.isAdoptedShell(callingUid, mContext)) {
            // 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 (isArchivingEnabled() && params.appPackageName != null) {
            PackageStateInternal ps = mPm.snapshotComputer().getPackageStateInternal(
                    params.appPackageName, SYSTEM_UID);
            if (ps != null
                    && PackageArchiver.isArchived(ps.getUserStateOrDefault(userId))
                    && TextUtils.equals(
                        PackageArchiver.getResponsibleInstallerPackage(ps),
                        requestedInstallerPackageName)) {
                params.installFlags |= PackageManager.INSTALL_UNARCHIVE;
            }
        }

        if ((params.installFlags & PackageManager.INSTALL_INSTANT_APP) != 0) {
            throw new SecurityException("Installing instant apps is forbidden");
        }

        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);
                }
            }
        }

        if (params.originatingUri != null
                && params.originatingUri.toString().length() > MAX_URI_LENGTH) {
            throw new IllegalArgumentException(
                    "Originating URI exceeds " + MAX_URI_LENGTH + " length limit");
        }

        if (params.referrerUri != null && params.referrerUri.toString().length() > MAX_URI_LENGTH) {
            throw new IllegalArgumentException(
                    "Referrer URI exceeds " + MAX_URI_LENGTH + " length limit");
        }

        if (params.whitelistedRestrictedPermissions != null) {
            params.whitelistedRestrictedPermissions.retainAll(
                    mPm.getAllPlatformRestrictedPermissions());
        }

        if (!validatePermissionStates(params.getPermissionStates())) {
            throw new IllegalArgumentException(
                    "Permissions states exceeds total size limit "
                            + MAX_PERMISSION_STATES_SIZE + " in length");
        }

        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);
        final int verificationPolicy;
        synchronized (mDeveloperVerificationPolicyPerUser) {
            verificationPolicy = mDeveloperVerificationPolicyPerUser.get(
                    userId, DEFAULT_VERIFICATION_POLICY);
        }
        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, "", null,
                mDeveloperVerifierController, verificationPolicy, verificationPolicy,
                mInstallDependencyHelper, /* restoredOnReboot= */ false);

        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;
    }

    private boolean validatePermissionStates(Map<String, Integer> permissionStates) {
        int totalLength = 0;
        for (String permission : permissionStates.keySet()) {
            totalLength += permission.length();
        }
        return totalLength <= MAX_PERMISSION_STATES_SIZE;
    }

    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 (!isArchivingEnabled() || 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);
    }

    /**
     * Check if a string is a valid package name
     */
    public 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;
        }
        if (Flags.verificationService()
                && mDeveloperVerifierController.getVerifierPackageName() != null) {
            // Developer verifier can always access session infos
            if (snapshot.getPackageUid(mDeveloperVerifierController.getVerifierPackageName(),
                    /* flags= */ 0, UserHandle.getUserId(uid)) == uid) {
                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
    @EnforcePermission(android.Manifest.permission.SET_DEVELOPER_VERIFICATION_USER_RESPONSE)
    public DeveloperVerificationUserConfirmationInfo getDeveloperVerificationUserConfirmationInfo(
            int sessionId) {
        getDeveloperVerificationUserConfirmationInfo_enforcePermission();
        final DeveloperVerificationUserConfirmationInfo result;
        synchronized (mSessions) {
            final PackageInstallerSession session = mSessions.get(sessionId);
            result = session != null
                    ? session.generateDeveloperVerificationUserConfirmationInfo()
                    : null;
        }
        return 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(),
                Binder.getCallingPid());
    }

    void uninstall(VersionedPackage versionedPackage, String callerPackageName, int flags,
            IntentSender statusReceiver, int userId, int callingUid, int callingPid) {
        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))
                        || PackageInstallerSession.isEmergencyInstallerEnabled(
                        versionedPackage.getPackageName(), snapshot, userId, callingUid);

        final PackageDeleteObserverAdapter adapter = new PackageDeleteObserverAdapter(mContext,
                statusReceiver, versionedPackage.getPackageName(),
                canSilentlyInstallPackage, userId, mPackageArchiver, flags);
        if (mContext.checkPermission(Manifest.permission.DELETE_PACKAGES, callingPid, callingUid)
                == PackageManager.PERMISSION_GRANTED) {
            // Sweet, call straight through!
            mPm.deletePackageVersioned(versionedPackage, adapter.getBinder(), userId, flags,
                    callingUid);
        } else if (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 if (isSystemAppProtectionRoleHolder(snapshot, userId, callingUid)) {
            // Allow the SYSTEM_APP_PROTECTION_SERVICE role holder to silently uninstall, with a
            // clean calling identity to get DELETE_PACKAGES permission
            Binder.withCleanCallingIdentity(() ->
                    mPm.deletePackageVersioned(versionedPackage, adapter.getBinder(), userId, flags)
            );
        } else {
            ApplicationInfo appInfo = snapshot.getApplicationInfo(callerPackageName, 0, userId);
            if (appInfo.targetSdkVersion >= Build.VERSION_CODES.P) {
                mContext.enforcePermission(Manifest.permission.REQUEST_DELETE_PACKAGES, callingPid,
                        callingUid, 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);
        }
    }

    private Boolean isSystemAppProtectionRoleHolder(
            @NonNull Computer snapshot, int userId, int callingUid) {
        String holderPackageName = Binder.withCleanCallingIdentity(() -> {
            RoleManager roleManager = mPm.mContext.getSystemService(RoleManager.class);
            if (roleManager == null) {
                return null;
            }
            List<String> holders = roleManager.getRoleHoldersAsUser(
                    ROLE_SYSTEM_APP_PROTECTION_SERVICE, UserHandle.of(userId));
            if (holders.isEmpty()) {
                return null;
            }
            return holders.get(0);
        });
        if (holderPackageName == null) {
            return false;
        }
        return snapshot.getPackageUid(holderPackageName, /* flags= */ 0, userId) == callingUid;
    }

    @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);
            }
        }
    }

    @android.annotation.EnforcePermission(android.Manifest.permission.INSTALL_GRANT_RUNTIME_PERMISSIONS)
    @Override
    public void updatePermissionStates(int sessionId, String[] permissionNames, int[] states) {
        updatePermissionStates_enforcePermission();

        synchronized (mSessions) {
            PackageInstallerSession session = mSessions.get(sessionId);
            if (session != null) {
                session.updatePermissionStates(permissionNames, states);
            }
        }
    }

    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 /* requiredPermission */, options.toBundle(),
                        null /* executor */, null /* onFinished*/);
            } 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,
            int flags,
            @NonNull IntentSender intentSender,
            @NonNull UserHandle userHandle) {
        mPackageArchiver.requestArchive(packageName, callerPackageName, flags, intentSender,
                userHandle);
    }

    @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);

        Slog.i(TAG,
                TextUtils.formatSimple("Requested archived install of package %s for user %s.",
                        archivedPackageParcel.packageName,
                        userHandle.getIdentifier()));
        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*/);
                Slog.i(TAG, TextUtils.formatSimple("Installed archived app %s.",
                        archivedPackageParcel.packageName));
            } 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));
            }

            session.reportUnarchivalStatus(status, unarchiveId, requiredStorageBytes,
                    userActionIntent);
        }
    }

    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);
        }
    }

    @Override
    @RequiresPermission(value = Manifest.permission.DEVELOPER_VERIFICATION_AGENT,
            conditional = true)
    public @PackageInstaller.DeveloperVerificationPolicy int getDeveloperVerificationPolicy(
            int userId) {
        final int callingUid = getCallingUid();
        final int callingPid = getCallingPid();
        if (mContext.checkPermission(Manifest.permission.DEVELOPER_VERIFICATION_AGENT, callingPid,
                callingUid) != PackageManager.PERMISSION_GRANTED) {
            if (!isCallerDeveloperVerificationPolicyDelegate(
                    mPm.snapshotComputer(), callingUid, userId)) {
                throw new SecurityException(
                        "Caller is not allowed to read the developer verification policy");
            }
        }
        synchronized (mDeveloperVerificationPolicyPerUser) {
            if (mDeveloperVerificationPolicyPerUser.indexOfKey(userId) < 0) {
                throw new IllegalStateException(
                        "Verification policy for user " + userId + " does not exist."
                                + " Does the user exist?");
            }
            return mDeveloperVerificationPolicyPerUser.get(userId);
        }
    }

    @Override
    public boolean setDeveloperVerificationPolicy(
            @PackageInstaller.DeveloperVerificationPolicy int policy, int userId) {
        // Write is more restrictive than read. Having the permission granted is not enough.
        // Only the verifier or the delegate app can change the developer verification policy.
        final int callingUid = getCallingUid();
        final Computer snapshot = mPm.snapshotComputer();
        if (!isCallerDeveloperVerifier(snapshot, callingUid, userId)
                && !isCallerDeveloperVerificationPolicyDelegate(snapshot, callingUid, userId)) {
            throw new SecurityException(
                    "Caller is not allowed to change the developer verification policy");
        }
        if (mDeveloperVerifierController.getVerifierPackageName() == null) {
            // The system doesn't have a specified verifier package.
            return false;
        }
        if (!isValidVerificationPolicy(policy)) {
            return false;
        }
        synchronized (mDeveloperVerificationPolicyPerUser) {
            if (mDeveloperVerificationPolicyPerUser.indexOfKey(userId) < 0) {
                throw new IllegalStateException(
                        "Verification policy for user " + userId + " does not exist."
                                + " Does the user exist?");
            }
            if (policy != mDeveloperVerificationPolicyPerUser.get(userId)) {
                mDeveloperVerificationPolicyPerUser.put(userId, policy);
                // Preserve the change to disk
                mSettingsWriteRequest.schedule();
            }
        }
        return true;
    }

    @Override
    @android.annotation.EnforcePermission(android.Manifest.permission.DEVELOPER_VERIFICATION_AGENT)
    @Nullable
    public String getDeveloperVerificationPolicyDelegatePackage(int userId) {
        getDeveloperVerificationPolicyDelegatePackage_enforcePermission();
        // This method can only be called by the verifier app
        final Computer snapshot = mPm.snapshotComputer();
        final int callingUid = getCallingUid();
        if (!isCallerDeveloperVerifier(snapshot, callingUid, userId)) {
            throw new SecurityException("The caller is not the developer verifier and cannot query"
                    + " the policy delegate app.");
        }
        final String delegatePackageName = mPm.getDeveloperVerificationPolicyDelegatePackageName();
        if (delegatePackageName == null) {
            return null;
        }
        // Check that the delegate app is installed and that the caller can see the delegate app.
        final PackageStateInternal delegatePs = snapshot.getPackageStateFiltered(
                delegatePackageName, callingUid, userId);
        if (delegatePs == null || !delegatePs.getUserStateOrDefault(userId).isInstalled()) {
            return null;
        }
        return delegatePs.getPackageName();
    }

    private boolean isCallerDeveloperVerifier(Computer snapshot, int callingUid, int userId) {
        final String verifierPackageName = mDeveloperVerifierController.getVerifierPackageName();
        if (verifierPackageName == null) {
            return false;
        }
        // Here we only care about the UID of the verifier app, so we don't do apps filter
        final int verifierUid = snapshot.getPackageUidInternal(
                verifierPackageName, 0 /* flags */, userId, SYSTEM_UID);
        return UserHandle.isSameApp(callingUid, verifierUid);
    }

    private boolean isCallerDeveloperVerificationPolicyDelegate(
            Computer snapshot, int callingUid, int userId) {
        final String delegatePackageName = mPm.getDeveloperVerificationPolicyDelegatePackageName();
        if (delegatePackageName == null) {
            return false;
        }
        // Here we only care about the UID of the delegate app, so we don't do apps filter
        final int delegateUid = snapshot.getPackageUidInternal(
                delegatePackageName, 0 /* flags */, userId, SYSTEM_UID);
        return UserHandle.isSameApp(callingUid, delegateUid);
    }

    @Override
    @Nullable
    public ComponentName getDeveloperVerificationServiceProvider() {
        final ComponentName verifierComponentName =
                mDeveloperVerifierController.getVerifierComponentName();
        if (verifierComponentName == null) {
            return null;
        }
        final int callingUid = Binder.getCallingUid();
        final Computer snapshot = mPm.snapshotComputer();
        if (!snapshot.canQueryPackage(callingUid, verifierComponentName.getPackageName())) {
            // Verifier package is not visible to the caller
            return null;
        }
        return verifierComponentName;
    }

    @Override
    public void addDeveloperVerificationExperiment(String packageName, int verificationPolicy,
            int[] results) {
        List<Integer> resultsList = new ArrayList<>(results.length);
        for (int i = 0; i < results.length; i++) {
            resultsList.add(results[i]);
        }
        mDeveloperVerifierController.addExperiment(packageName, verificationPolicy, resultsList);
    }

    @Override
    public void clearDeveloperVerificationExperiment(String packageName) {
        mDeveloperVerifierController.clearExperiment(packageName);
    }

    void onUserAdded(int userId) {
        synchronized (mDeveloperVerificationPolicyPerUser) {
            mDeveloperVerificationPolicyPerUser.put(userId, DEFAULT_VERIFICATION_POLICY);
        }
    }

    void onUserRemoved(int userId) {
        synchronized (mDeveloperVerificationPolicyPerUser) {
            mDeveloperVerificationPolicyPerUser.delete(userId);
        }
    }

    @Override
    @EnforcePermission(android.Manifest.permission.SET_DEVELOPER_VERIFICATION_USER_RESPONSE)
    public void setDeveloperVerificationUserResponse(int sessionId,
            @PackageInstaller.DeveloperVerificationUserResponse int userResponse) {
        setDeveloperVerificationUserResponse_enforcePermission();

        synchronized (mSessions) {
            PackageInstallerSession session = mSessions.get(sessionId);
            if (session != null) {
                session.setVerificationUserResponse(userResponse);
            } else {
                Slog.e(TAG, "Session " + sessionId + " not found");
            }
        }
    }

    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;
        private final int mUserId;

        @DeleteFlags
        private final int mFlags;

        @Nullable
        private final PackageArchiver mPackageArchiver;

        PackageDeleteObserverAdapter(Context context, IntentSender target,
                String packageName, boolean showNotification, int userId) {
            this(context, target, packageName, showNotification, userId,
                    /* packageArchiver= */ null, /* flags= */ 0);
        }

        PackageDeleteObserverAdapter(Context context, IntentSender target,
                String packageName, boolean showNotification, int userId,
                PackageArchiver packageArchiver, @DeleteFlags int flags) {
            mContext = context;
            mTarget = target;
            mPackageName = packageName;
            if (showNotification) {
                mNotification = buildSuccessNotification(mContext,
                        getDeviceOwnerDeletedPackageMsg(),
                        packageName,
                        userId);
            } else {
                mNotification = null;
            }
            mUserId = userId;
            mPackageArchiver = packageArchiver;
            mFlags = flags;
        }

        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 /* requiredPermission */, options.toBundle(),
                        null /* executor */, null /* onFinished*/);
            } 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 (mPackageArchiver != null
                    && PackageManager.DELETE_SUCCEEDED != returnCode
                    && (mFlags & DELETE_ARCHIVE) != 0) {
                mPackageArchiver.clearArchiveState(mPackageName, mUserId);
            }
            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 /* requiredPermission */, options.toBundle(),
                        null /* executor */, null /* onFinished*/);
            } 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);
        synchronized (mDeveloperVerificationPolicyPerUser) {
            pw.printPair("VerificationPolicyPerUser",
                    mDeveloperVerificationPolicyPerUser.toString());
        }
    }

    @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, /* isSessionExpired= */ false);
                            }
                        }

                        if (Flags.sdkDependencyInstaller()) {
                            mInstallDependencyHelper.notifySessionComplete(session.sessionId);
                        }

                        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;
        }
    }
}
