/*
 * Copyright (C) 2021 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 com.android.internal.pm.pkg.parsing.ParsingPackageUtils.PARSE_APEX;

import android.apex.ApexInfo;
import android.apex.ApexInfoList;
import android.apex.ApexSessionInfo;
import android.apex.ApexSessionParams;
import android.content.Context;
import android.content.Intent;
import android.content.pm.IPackageInstallObserver2;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.SigningDetails;
import android.content.pm.parsing.result.ParseResult;
import android.content.pm.parsing.result.ParseTypeImpl;
import android.content.rollback.RollbackInfo;
import android.content.rollback.RollbackManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.RemoteException;
import android.os.UserHandle;
import android.util.IntArray;
import android.util.Slog;
import android.util.apk.ApkSignatureVerifier;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.content.InstallLocationUtils;
import com.android.internal.pm.parsing.PackageParser2;
import com.android.internal.pm.parsing.PackageParserException;
import com.android.internal.pm.parsing.pkg.ParsedPackage;
import com.android.server.LocalServices;
import com.android.server.SystemConfig;
import com.android.server.rollback.RollbackManagerInternal;

import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;

final class PackageSessionVerifier {
    private static final String TAG = "PackageSessionVerifier";

    interface Callback {
        void onResult(int returnCode, String msg);
    }

    private final Context mContext;
    private final PackageManagerService mPm;
    private final ApexManager mApexManager;
    private final Supplier<PackageParser2> mPackageParserSupplier;
    // The handler thread to run verification tasks. Since all tasks run in the same thread,
    // there is no need for synchronization.
    private final Handler mHandler;
    // Parent sessions for checking session conflicts.
    private final List<StagingManager.StagedSession> mStagedSessions = new ArrayList<>();

    PackageSessionVerifier(Context context, PackageManagerService pm,
            ApexManager apexManager, Supplier<PackageParser2> packageParserSupplier,
            Looper looper) {
        mContext = context;
        mPm = pm;
        mApexManager = apexManager;
        mPackageParserSupplier = packageParserSupplier;
        mHandler = new Handler(looper);
    }

    @VisibleForTesting
    PackageSessionVerifier() {
        mContext = null;
        mPm = null;
        mApexManager = null;
        mPackageParserSupplier = null;
        mHandler = null;
    }

    /**
     * Runs verifications that are common to both staged and non-staged sessions.
     */
    public void verify(PackageInstallerSession session, Callback callback) {
        mHandler.post(() -> {
            try {
                storeSession(session.mStagedSession);
                if (session.isMultiPackage()) {
                    for (PackageInstallerSession child : session.getChildSessions()) {
                        checkApexUpdateAllowed(child);
                        checkRebootlessApex(child);
                        checkApexSignature(child);
                    }
                } else {
                    checkApexUpdateAllowed(session);
                    checkRebootlessApex(session);
                    checkApexSignature(session);
                }
                verifyAPK(session, callback);
            } catch (PackageManagerException e) {
                String errorMessage = PackageManager.installStatusToString(e.error, e.getMessage());
                session.setSessionFailed(e.error, errorMessage);
                callback.onResult(e.error, e.getMessage());
            }
        });
    }

    private SigningDetails getSigningDetails(PackageInfo apexPkg) throws PackageManagerException {
        final String apexPath = apexPkg.applicationInfo.sourceDir;
        final int minSignatureScheme =
                ApkSignatureVerifier.getMinimumSignatureSchemeVersionForTargetSdk(
                        apexPkg.applicationInfo.targetSdkVersion);
        final ParseTypeImpl input = ParseTypeImpl.forDefaultParsing();
        final ParseResult<SigningDetails> result = ApkSignatureVerifier.verify(
                input, apexPath, minSignatureScheme);
        if (result.isError()) {
            throw new PackageManagerException(PackageManager.INSTALL_FAILED_VERIFICATION_FAILURE,
                    "Failed to verify APEX package " + apexPath + " : "
                            + result.getException(), result.getException());
        }
        return result.getResult();
    }

    private void checkApexSignature(PackageInstallerSession session)
            throws PackageManagerException {
        if (!session.isApexSession()) {
            return;
        }
        final String packageName = session.getPackageName();
        final PackageInfo existingApexPkg = mPm.snapshotComputer().getPackageInfo(
                session.getPackageName(), PackageManager.MATCH_APEX, UserHandle.USER_SYSTEM);
        if (existingApexPkg == null) {
            throw new PackageManagerException(PackageManager.INSTALL_FAILED_PACKAGE_CHANGED,
                    "Attempting to install new APEX package " + packageName);
        }
        final SigningDetails existingSigningDetails = getSigningDetails(existingApexPkg);
        final SigningDetails newSigningDetails = session.getSigningDetails();
        if (newSigningDetails.checkCapability(existingSigningDetails,
                SigningDetails.CertCapabilities.INSTALLED_DATA)
                || existingSigningDetails.checkCapability(newSigningDetails,
                SigningDetails.CertCapabilities.ROLLBACK)) {
            return;
        }
        throw new PackageManagerException(PackageManager.INSTALL_FAILED_VERIFICATION_FAILURE,
                "APK container signature of APEX package " + packageName
                        + " is not compatible with the one currently installed on device");
    }

    /**
     * Runs verifications particular to APK. This includes APEX sessions since an APEX can also
     * be treated as APK.
     */
    private void verifyAPK(PackageInstallerSession session, Callback callback)
            throws PackageManagerException {
        final IPackageInstallObserver2 observer = new IPackageInstallObserver2.Stub() {
            @Override
            public void onUserActionRequired(Intent intent) {
                throw new IllegalStateException();
            }
            @Override
            public void onPackageInstalled(String basePackageName, int returnCode, String msg,
                    Bundle extras) {
                if (session.isStaged() && returnCode == PackageManager.INSTALL_SUCCEEDED) {
                    // Continue verification for staged sessions
                    verifyStaged(session.mStagedSession, callback);
                    return;
                }
                if (returnCode != PackageManager.INSTALL_SUCCEEDED) {
                    String errorMessage = PackageManager.installStatusToString(returnCode, msg);
                    session.setSessionFailed(returnCode, errorMessage);
                    callback.onResult(returnCode, msg);
                } else {
                    session.setSessionReady();
                    callback.onResult(PackageManager.INSTALL_SUCCEEDED, null);
                }
            }
        };
        final VerifyingSession verifyingSession = createVerifyingSession(session, observer);
        if (session.isMultiPackage()) {
            final List<PackageInstallerSession> childSessions = session.getChildSessions();
            List<VerifyingSession> verifyingChildSessions = new ArrayList<>(childSessions.size());
            for (PackageInstallerSession child : childSessions) {
                verifyingChildSessions.add(createVerifyingSession(child, null));
            }
            verifyingSession.verifyStage(verifyingChildSessions);
        } else {
            verifyingSession.verifyStage();
        }
    }

    private VerifyingSession createVerifyingSession(
            PackageInstallerSession session, IPackageInstallObserver2 observer) {
        final UserHandle user;
        if ((session.params.installFlags & PackageManager.INSTALL_ALL_USERS) != 0) {
            user = UserHandle.ALL;
        } else {
            user = new UserHandle(session.userId);
        }
        return new VerifyingSession(user, session.stageDir, observer, session.params,
                session.getInstallSource(), session.getInstallerUid(), session.getSigningDetails(),
                session.sessionId, session.getPackageLite(), session.getUserActionRequired(), mPm);
    }

    /**
     * Starts pre-reboot verification for the staged-session. This operation is broken into the
     * following phases:
     * <ul>
     *     <li>Checks if multiple active sessions are supported.</li>
     *     <li>Checks conflicts in rollbacks and overlapping packages.</li>
     *     <li>Submits apex sessions to apex service.</li>
     *     <li>Validates signatures of apex files.</li>
     *     <li>Notifies the result of verification.</li>
     * </ul>
     *
     * Note it is the responsibility of the caller to ensure the staging files remain unchanged
     * while the verification is in progress.
     */
    private void verifyStaged(StagingManager.StagedSession session, Callback callback) {
        Slog.d(TAG, "Starting preRebootVerification for session " + session.sessionId());
        mHandler.post(() -> {
            try {
                checkActiveSessions();
                checkRollbacks(session);
                if (session.isMultiPackage()) {
                    for (StagingManager.StagedSession child : session.getChildSessions()) {
                        checkOverlaps(session, child);
                    }
                } else {
                    checkOverlaps(session, session);
                }
                dispatchVerifyApex(session, callback);
            } catch (PackageManagerException e) {
                onVerificationFailure(session, callback, e.error, e.getMessage());
            }
        });
    }

    /**
     * Stores staged-sessions for checking package overlapping and rollback conflicts.
     */
    @VisibleForTesting
    void storeSession(StagingManager.StagedSession session) {
        if (session != null) {
            mStagedSessions.add(session);
        }
    }

    private void onVerificationSuccess(StagingManager.StagedSession session, Callback callback) {
        callback.onResult(PackageManager.INSTALL_SUCCEEDED, null);
    }

    private void onVerificationFailure(StagingManager.StagedSession session, Callback callback,
            int errorCode, String errorMessage) {
        if (!ensureActiveApexSessionIsAborted(session)) {
            Slog.e(TAG, "Failed to abort apex session " + session.sessionId());
            // Safe to ignore active apex session abortion failure since session will be marked
            // failed on next step and staging directory for session will be deleted.
        }
        session.setSessionFailed(errorCode, errorMessage);
        callback.onResult(PackageManager.INSTALL_FAILED_VERIFICATION_FAILURE, errorMessage);
    }

    private void dispatchVerifyApex(StagingManager.StagedSession session, Callback callback) {
        mHandler.post(() -> {
            try {
                verifyApex(session);
                dispatchEndVerification(session, callback);
            } catch (PackageManagerException e) {
                onVerificationFailure(session, callback, e.error, e.getMessage());
            }
        });
    }

    private void dispatchEndVerification(StagingManager.StagedSession session, Callback callback) {
        mHandler.post(() -> {
            try {
                endVerification(session);
                onVerificationSuccess(session, callback);
            } catch (PackageManagerException e) {
                onVerificationFailure(session, callback, e.error, e.getMessage());
            }
        });
    }

    /**
     * Pre-reboot verification phase for apex files:
     *
     * <p><ul>
     *     <li>submits session to apex service</li>
     *     <li>validates signatures of apex files</li>
     * </ul></p>
     */
    private void verifyApex(StagingManager.StagedSession session) throws PackageManagerException {
        int rollbackId = -1;
        if ((session.sessionParams().installFlags & PackageManager.INSTALL_ENABLE_ROLLBACK) != 0) {
            // If rollback is enabled for this session, we call through to the RollbackManager
            // with the list of sessions it must enable rollback for. Note that
            // notifyStagedSession is a synchronous operation.
            final RollbackManagerInternal rm =
                    LocalServices.getService(RollbackManagerInternal.class);
            try {
                // NOTE: To stay consistent with the non-staged install flow, we don't fail the
                // entire install if rollbacks can't be enabled.
                rollbackId = rm.notifyStagedSession(session.sessionId());
            } catch (RuntimeException re) {
                Slog.e(TAG, "Failed to notifyStagedSession for session: "
                        + session.sessionId(), re);
            }
        } else if (isRollback(session)) {
            rollbackId = retrieveRollbackIdForCommitSession(session.sessionId());
        }

        final boolean hasApex = session.containsApexSession();
        // APEX checks. For single-package sessions, check if they contain an APEX. For
        // multi-package sessions, find all the child sessions that contain an APEX.
        if (hasApex) {
            submitSessionToApexService(session, rollbackId);
        }
    }

    /**
     * Pre-reboot verification phase for wrapping up:
     * <p><ul>
     *     <li>enables checkpoint if supported</li>
     *     <li>marks session as ready</li>
     * </ul></p>
     */
    private void endVerification(StagingManager.StagedSession session)
            throws PackageManagerException {
        // Before marking the session as ready, start checkpoint service if available
        try {
            if (InstallLocationUtils.getStorageManager().supportsCheckpoint()) {
                InstallLocationUtils.getStorageManager().startCheckpoint(2);
            }
        } catch (Exception e) {
            // Failed to get hold of StorageManager
            Slog.e(TAG, "Failed to get hold of StorageManager", e);
            throw new PackageManagerException(
                    PackageManager.INSTALL_FAILED_INTERNAL_ERROR,
                    "Failed to get hold of StorageManager");
        }
        // Proactively mark session as ready before calling apexd. Although this call order
        // looks counter-intuitive, this is the easiest way to ensure that session won't end up
        // in the inconsistent state:
        //  - If device gets rebooted right before call to apexd, then apexd will never activate
        //      apex files of this staged session. This will result in StagingManager failing
        //      the session.
        // On the other hand, if the order of the calls was inverted (first call apexd, then
        // mark session as ready), then if a device gets rebooted right after the call to apexd,
        // only apex part of the train will be applied, leaving device in an inconsistent state.
        Slog.d(TAG, "Marking session " + session.sessionId() + " as ready");
        session.setSessionReady();
        // If the session is abandoned while it's being staged/verified, the session can't be marked
        // as ready. Let's abort the session by throwing PackageManagerException. The apex session
        // will be aborted as well in the exception handler.
        if (session.isSessionReady()) {
            final boolean hasApex = session.containsApexSession();
            if (hasApex) {
                mApexManager.markStagedSessionReady(session.sessionId());
            }
        } else {
            Slog.e(TAG, "Failed to mark session " + session.sessionId() + " as ready.");
            throw new PackageManagerException(PackageManager.INSTALL_FAILED_INTERNAL_ERROR,
                    "Session is abandoned.");
        }
    }

    private void submitSessionToApexService(StagingManager.StagedSession session,
            int rollbackId) throws PackageManagerException {
        final IntArray childSessionIds = new IntArray();
        if (session.isMultiPackage()) {
            for (StagingManager.StagedSession s : session.getChildSessions()) {
                if (s.isApexSession()) {
                    childSessionIds.add(s.sessionId());
                }
            }
        }
        ApexSessionParams apexSessionParams = new ApexSessionParams();
        apexSessionParams.sessionId = session.sessionId();
        apexSessionParams.childSessionIds = childSessionIds.toArray();
        if (session.sessionParams().installReason == PackageManager.INSTALL_REASON_ROLLBACK) {
            apexSessionParams.isRollback = true;
            apexSessionParams.rollbackId = rollbackId;
        } else {
            if (rollbackId != -1) {
                apexSessionParams.hasRollbackEnabled = true;
                apexSessionParams.rollbackId = rollbackId;
            }
        }
        // submitStagedSession will throw a PackageManagerException if apexd verification fails,
        // which will be propagated to populate stagedSessionErrorMessage of this session.
        final ApexInfoList apexInfoList = mApexManager.submitStagedSession(apexSessionParams);
        final List<String> apexPackageNames = new ArrayList<>();
        for (ApexInfo apexInfo : apexInfoList.apexInfos) {
            final ParsedPackage parsedPackage;
            try (PackageParser2 packageParser = mPackageParserSupplier.get()) {
                File apexFile = new File(apexInfo.modulePath);
                parsedPackage = packageParser.parsePackage(apexFile, PARSE_APEX, false);
            } catch (PackageParserException e) {
                throw new PackageManagerException(
                        PackageManager.INSTALL_FAILED_VERIFICATION_FAILURE,
                        "Failed to parse APEX package " + apexInfo.modulePath + " : " + e, e);
            }
            apexPackageNames.add(parsedPackage.getPackageName());
        }
        Slog.d(TAG, "Session " + session.sessionId() + " has following APEX packages: "
                + apexPackageNames);
    }

    private int retrieveRollbackIdForCommitSession(int sessionId) throws PackageManagerException {
        RollbackManager rm = mContext.getSystemService(RollbackManager.class);

        final List<RollbackInfo> rollbacks = rm.getRecentlyCommittedRollbacks();
        for (int i = 0, size = rollbacks.size(); i < size; i++) {
            final RollbackInfo rollback = rollbacks.get(i);
            if (rollback.getCommittedSessionId() == sessionId) {
                return rollback.getRollbackId();
            }
        }
        throw new PackageManagerException(
                PackageManager.INSTALL_FAILED_VERIFICATION_FAILURE,
                "Could not find rollback id for commit session: " + sessionId);
    }

    private static boolean isRollback(StagingManager.StagedSession session) {
        return session.sessionParams().installReason == PackageManager.INSTALL_REASON_ROLLBACK;
    }

    private static boolean isApexSessionFinalized(ApexSessionInfo info) {
        /* checking if the session is in a final state, i.e., not active anymore */
        return info.isUnknown || info.isActivationFailed || info.isSuccess
                || info.isReverted;
    }

    private boolean ensureActiveApexSessionIsAborted(StagingManager.StagedSession session) {
        if (!session.containsApexSession()) {
            return true;
        }
        final int sessionId = session.sessionId();
        final ApexSessionInfo apexSession = mApexManager.getStagedSessionInfo(sessionId);
        if (apexSession == null || isApexSessionFinalized(apexSession)) {
            return true;
        }
        return mApexManager.abortStagedSession(sessionId);
    }

    private boolean isApexUpdateAllowed(String apexPackageName, String installerPackageName) {
        if (mPm.getModuleInfo(apexPackageName, 0) != null) {
            final String modulesInstaller =
                    SystemConfig.getInstance().getModulesInstallerPackageName();
            if (modulesInstaller == null) {
                Slog.w(TAG, "No modules installer defined");
                return false;
            }
            return modulesInstaller.equals(installerPackageName);
        }
        final String vendorApexInstaller =
                SystemConfig.getInstance().getAllowedVendorApexes().get(apexPackageName);
        if (vendorApexInstaller == null) {
            Slog.w(TAG, apexPackageName + " is not allowed to be updated");
            return false;
        }
        return vendorApexInstaller.equals(installerPackageName);
    }

    /**
     * Checks if APEX update is allowed.
     *
     * This phase is shared between staged and non-staged sessions and should be called after
     * boot is completed since this check depends on the ModuleInfoProvider, which is only populated
     * after device has booted.
     */
    private void checkApexUpdateAllowed(PackageInstallerSession session)
            throws PackageManagerException {
        if (!session.isApexSession()) {
            return;
        }
        final int installFlags = session.params.installFlags;
        if ((installFlags & PackageManager.INSTALL_DISABLE_ALLOWED_APEX_UPDATE_CHECK) != 0) {
            return;
        }
        final String packageName = session.getPackageName();
        final String installerPackageName = session.getInstallSource().mInstallerPackageName;
        if (!isApexUpdateAllowed(packageName, installerPackageName)) {
            throw new PackageManagerException(
                    PackageManager.INSTALL_FAILED_VERIFICATION_FAILURE,
                    "Update of APEX package " + packageName + " is not allowed for "
                            + installerPackageName);
        }
    }

    /**
     * Fails this rebootless APEX session if the same package name found in any staged sessions.
     */
    @VisibleForTesting
    void checkRebootlessApex(PackageInstallerSession session)
            throws PackageManagerException {
        if (session.isStaged() || !session.isApexSession()) {
            return;
        }
        String packageName = session.getPackageName();
        if (packageName == null) {
            throw new PackageManagerException(
                    PackageManager.INSTALL_FAILED_VERIFICATION_FAILURE,
                    "Invalid session " + session.sessionId + " with package name null");
        }
        for (StagingManager.StagedSession stagedSession : mStagedSessions) {
            if (stagedSession.isDestroyed() || stagedSession.isInTerminalState()) {
                continue;
            }
            if (stagedSession.sessionContains(s -> packageName.equals(s.getPackageName()))) {
                // Staged-sessions take priority over rebootless APEX
                throw new PackageManagerException(
                        PackageManager.INSTALL_FAILED_VERIFICATION_FAILURE,
                        "Staged session " + stagedSession.sessionId() + " already contains "
                                + packageName);
            }
        }
    }

    /**
     * Checks if multiple staged-sessions are supported. It is supported only when the system
     * supports checkpoint.
     */
    private void checkActiveSessions() throws PackageManagerException {
        try {
            checkActiveSessions(InstallLocationUtils.getStorageManager().supportsCheckpoint());
        } catch (RemoteException e) {
            throw new PackageManagerException(PackageManager.INSTALL_FAILED_INTERNAL_ERROR,
                    "Can't query fs-checkpoint status : " + e);
        }
    }

    @VisibleForTesting
    void checkActiveSessions(boolean supportsCheckpoint) throws PackageManagerException {
        int activeSessions = 0;
        for (StagingManager.StagedSession stagedSession : mStagedSessions) {
            if (stagedSession.isDestroyed() || stagedSession.isInTerminalState()) {
                continue;
            }
            ++activeSessions;
        }
        if (!supportsCheckpoint && activeSessions > 1) {
            throw new PackageManagerException(
                    PackageManager.INSTALL_FAILED_OTHER_STAGED_SESSION_IN_PROGRESS,
                    "Cannot stage multiple sessions without checkpoint support");
        }
    }

    /**
     * Fails non-rollback sessions if any rollback session exists. A rollback session might cause
     * downgrade of SDK extension which in turn will result in dependency violation of other
     * non-rollback sessions.
     */
    @VisibleForTesting
    void checkRollbacks(StagingManager.StagedSession session)
            throws PackageManagerException {
        if (session.isDestroyed() || session.isInTerminalState()) {
            return;
        }
        for (StagingManager.StagedSession stagedSession : mStagedSessions) {
            if (stagedSession.isDestroyed() || stagedSession.isInTerminalState()) {
                continue;
            }
            if (isRollback(session) && !isRollback(stagedSession)) {
                // If the new session is a rollback, then it gets priority. The existing
                // session is failed to reduce risk and avoid an SDK extension dependency
                // violation.
                if (!ensureActiveApexSessionIsAborted(stagedSession)) {
                    Slog.e(TAG, "Failed to abort apex session " + stagedSession.sessionId());
                    // Safe to ignore active apex session abort failure since session
                    // will be marked failed on next step and staging directory for session
                    // will be deleted.
                }
                stagedSession.setSessionFailed(
                        PackageManager.INSTALL_FAILED_OTHER_STAGED_SESSION_IN_PROGRESS,
                        "Session was failed by rollback session: " + session.sessionId());
                Slog.i(TAG, "Session " + stagedSession.sessionId() + " is marked failed due to "
                        + "rollback session: " + session.sessionId());
            } else if (!isRollback(session) && isRollback(stagedSession)) {
                throw new PackageManagerException(
                        PackageManager.INSTALL_FAILED_OTHER_STAGED_SESSION_IN_PROGRESS,
                        "Session was failed by rollback session: " + stagedSession.sessionId());

            }
        }
    }

    /**
     * Fails the session that is committed later when overlapping packages are detected.
     *
     * @param parent The parent session.
     * @param child The child session whose package name will be checked.
     *              This will equal to {@code parent} for a single-package session.
     */
    @VisibleForTesting
    void checkOverlaps(StagingManager.StagedSession parent,
            StagingManager.StagedSession child) throws PackageManagerException {
        if (parent.isDestroyed() || parent.isInTerminalState()) {
            return;
        }
        final String packageName = child.getPackageName();
        if (packageName == null) {
            throw new PackageManagerException(
                    PackageManager.INSTALL_FAILED_VERIFICATION_FAILURE,
                    "Cannot stage session " + child.sessionId() + " with package name null");
        }
        for (StagingManager.StagedSession stagedSession : mStagedSessions) {
            if (stagedSession.isDestroyed() || stagedSession.isInTerminalState()
                    || stagedSession.sessionId() == parent.sessionId()) {
                continue;
            }
            if (stagedSession.sessionContains(s -> packageName.equals(s.getPackageName()))) {
                if (stagedSession.getCommittedMillis() < parent.getCommittedMillis()) {
                    // Fail the session committed later when there are overlapping packages
                    throw new PackageManagerException(
                            PackageManager.INSTALL_FAILED_OTHER_STAGED_SESSION_IN_PROGRESS,
                            "Package: " + packageName + " in session: "
                                    + child.sessionId()
                                    + " has been staged already by session: "
                                    + stagedSession.sessionId());
                } else {
                    stagedSession.setSessionFailed(
                            PackageManager.INSTALL_FAILED_OTHER_STAGED_SESSION_IN_PROGRESS,
                            "Package: " + packageName + " in session: "
                                    + stagedSession.sessionId()
                                    + " has been staged already by session: "
                                    + child.sessionId());
                }
            }
        }
    }
}
