Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit b0ff86ef authored by Jackal Guo's avatar Jackal Guo
Browse files

Pre-commit install approval (2/n)

Introduce a new mechanism to allow developers requesting the approval
from users for an install before committing the session and without
any of the APK file.

Bug: 242677131
Test: atest CtsPackageInstallTestCases
Change-Id: I38029d9dbf28f03b81e4988f1f864e552129c957
parent ea765a3f
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -11595,6 +11595,7 @@ package android.content.pm {
    field public static final String ACTION_SESSION_UPDATED = "android.content.pm.action.SESSION_UPDATED";
    field public static final String EXTRA_OTHER_PACKAGE_NAME = "android.content.pm.extra.OTHER_PACKAGE_NAME";
    field public static final String EXTRA_PACKAGE_NAME = "android.content.pm.extra.PACKAGE_NAME";
    field public static final String EXTRA_PRE_APPROVAL = "android.content.pm.extra.PRE_APPROVAL";
    field public static final String EXTRA_SESSION = "android.content.pm.extra.SESSION";
    field public static final String EXTRA_SESSION_ID = "android.content.pm.extra.SESSION_ID";
    field public static final String EXTRA_STATUS = "android.content.pm.extra.STATUS";
@@ -11651,6 +11652,7 @@ package android.content.pm {
    method public void removeChildSessionId(int);
    method public void removeSplit(@NonNull String) throws java.io.IOException;
    method public void requestChecksums(@NonNull String, int, @NonNull java.util.List<java.security.cert.Certificate>, @NonNull java.util.concurrent.Executor, @NonNull android.content.pm.PackageManager.OnChecksumsReadyListener) throws java.security.cert.CertificateEncodingException, java.io.FileNotFoundException;
    method public void requestUserPreapproval(@NonNull android.content.pm.PackageInstaller.PreapprovalDetails, @NonNull android.content.IntentSender);
    method @Deprecated public void setChecksums(@NonNull String, @NonNull java.util.List<android.content.pm.Checksum>, @Nullable byte[]) throws java.io.IOException;
    method public void setStagingProgress(float);
    method public void transfer(@NonNull String) throws android.content.pm.PackageManager.NameNotFoundException;
+3 −0
Original line number Diff line number Diff line
@@ -20,6 +20,7 @@ import android.content.pm.Checksum;
import android.content.pm.DataLoaderParamsParcel;
import android.content.pm.IOnChecksumsReadyListener;
import android.content.pm.IPackageInstallObserver2;
import android.content.pm.PackageInstaller;
import android.content.IntentSender;
import android.os.ParcelFileDescriptor;

@@ -58,4 +59,6 @@ interface IPackageInstallerSession {

    boolean isStaged();
    int getInstallFlags();

    void requestUserPreapproval(in PackageInstaller.PreapprovalDetails details, in IntentSender statusReceiver);
}
+55 −0
Original line number Diff line number Diff line
@@ -170,6 +170,10 @@ public class PackageInstaller {
    /** {@hide} */
    public static final String ACTION_CONFIRM_INSTALL = "android.content.pm.action.CONFIRM_INSTALL";

    /** @hide */
    public static final String ACTION_CONFIRM_PRE_APPROVAL =
            "android.content.pm.action.CONFIRM_PRE_APPROVAL";

    /**
     * An integer session ID that an operation is working with.
     *
@@ -206,6 +210,17 @@ public class PackageInstaller {
     */
    public static final String EXTRA_STATUS = "android.content.pm.extra.STATUS";

    /**
     * Indicate if the status is for a pre-approval request.
     *
     * If callers use the same {@link IntentSender} for both
     * {@link Session#requestUserPreapproval(PreapprovalDetails, IntentSender)} and
     * {@link Session#commit(IntentSender)}, they can use this to differentiate between them.
     *
     * @see Intent#getBooleanExtra(String, boolean)
     */
    public static final String EXTRA_PRE_APPROVAL = "android.content.pm.extra.PRE_APPROVAL";

    /**
     * Detailed string representation of the status, including raw details that
     * are useful for debugging.
@@ -1667,6 +1682,41 @@ public class PackageInstaller {
                e.rethrowFromSystemServer();
            }
        }

        /**
         * Attempt to request the approval before committing this session.
         *
         * For installers that have been granted the
         * {@link android.Manifest.permission#REQUEST_INSTALL_PACKAGES REQUEST_INSTALL_PACKAGES}
         * permission, they can request the approval from users before
         * {@link Session#commit(IntentSender)} is called. This may require user intervention as
         * well. The result of the request will be reported through the given callback.
         *
         * @param details the adequate context to this session for requesting the approval from
         *                users prior to commit.
         * @param statusReceiver called when the state of the session changes.
         *                       Intents sent to this receiver contain
         *                       {@link #EXTRA_STATUS}. Refer to the individual
         *                       status codes on how to handle them.
         *
         * @throws IllegalArgumentException when {@link PreapprovalDetails} is {@code null}.
         * @throws IllegalArgumentException if {@link IntentSender} is {@code null}.
         * @throws IllegalStateException if called on a multi-package session (no matter
         *                               the parent session or any of the children sessions).
         * @throws IllegalStateException if called again after this method has been called on
         *                               this session.
         * @throws SecurityException when the caller does not own this session.
         */
        public void requestUserPreapproval(@NonNull PreapprovalDetails details,
                @NonNull IntentSender statusReceiver) {
            Preconditions.checkArgument(details != null, "preapprovalDetails cannot be null.");
            Preconditions.checkArgument(statusReceiver != null, "statusReceiver cannot be null.");
            try {
                mSession.requestUserPreapproval(details, statusReceiver);
            } catch (RemoteException e) {
                e.rethrowFromSystemServer();
            }
        }
    }

    /**
@@ -2631,6 +2681,9 @@ public class PackageInstaller {
        /** {@hide} */
        public int installerUid;

        /** @hide */
        public boolean isPreapprovalRequested;

        /** {@hide} */
        @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
        public SessionInfo() {
@@ -2678,6 +2731,7 @@ public class PackageInstaller {
            mSessionErrorCode = source.readInt();
            mSessionErrorMessage = source.readString();
            isCommitted = source.readBoolean();
            isPreapprovalRequested = source.readBoolean();
            rollbackDataPolicy = source.readInt();
            createdMillis = source.readLong();
            requireUserAction = source.readInt();
@@ -3257,6 +3311,7 @@ public class PackageInstaller {
            dest.writeInt(mSessionErrorCode);
            dest.writeString(mSessionErrorMessage);
            dest.writeBoolean(isCommitted);
            dest.writeBoolean(isPreapprovalRequested);
            dest.writeInt(rollbackDataPolicy);
            dest.writeLong(createdMillis);
            dest.writeInt(requireUserAction);
+206 −14
Original line number Diff line number Diff line
@@ -81,9 +81,11 @@ import android.content.pm.InstallationFile;
import android.content.pm.InstallationFileParcel;
import android.content.pm.PackageInfo;
import android.content.pm.PackageInstaller;
import android.content.pm.PackageInstaller.PreapprovalDetails;
import android.content.pm.PackageInstaller.SessionInfo;
import android.content.pm.PackageInstaller.SessionParams;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.PackageInfoFlags;
import android.content.pm.PackageManagerInternal;
import android.content.pm.SigningDetails;
import android.content.pm.dex.DexMetadataHelper;
@@ -92,8 +94,13 @@ import android.content.pm.parsing.ApkLiteParseUtils;
import android.content.pm.parsing.PackageLite;
import android.content.pm.parsing.result.ParseResult;
import android.content.pm.parsing.result.ParseTypeImpl;
import android.content.res.ApkAssets;
import android.content.res.AssetManager;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.icu.util.ULocale;
import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
@@ -193,6 +200,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
    private static final int MSG_INSTALL = 3;
    private static final int MSG_ON_PACKAGE_INSTALLED = 4;
    private static final int MSG_SESSION_VALIDATION_FAILURE = 5;
    private static final int MSG_PRE_APPROVAL_REQUEST = 6;

    /** XML constants used for persisting a session */
    static final String TAG_SESSION = "session";
@@ -360,6 +368,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
    @GuardedBy("mLock")
    private boolean mShouldBeSealed = false;

    private final AtomicBoolean mPreapprovalRequested = new AtomicBoolean(false);
    private final AtomicBoolean mCommitted = new AtomicBoolean(false);

    /**
@@ -387,6 +396,9 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
    @GuardedBy("mLock")
    private IntentSender mRemoteStatusReceiver;

    @GuardedBy("mLock")
    private PreapprovalDetails mPreapprovalDetails;

    /** Fields derived from commit parsing */
    @GuardedBy("mLock")
    private String mPackageName;
@@ -740,11 +752,12 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
                    final Bundle extras = (Bundle) args.arg3;
                    final IntentSender statusReceiver = (IntentSender) args.arg4;
                    final int returnCode = args.argi1;
                    final boolean isPreapproval = args.argi2 == 1;
                    args.recycle();

                    sendOnPackageInstalled(mContext, statusReceiver, sessionId,
                            isInstallerDeviceOwnerOrAffiliatedProfileOwner(), userId,
                            packageName, returnCode, message, extras);
                            packageName, returnCode, isPreapproval, message, extras);

                    break;
                case MSG_SESSION_VALIDATION_FAILURE:
@@ -752,6 +765,9 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
                    final String detailMessage = (String) msg.obj;
                    onSessionValidationFailure(error, detailMessage);
                    break;
                case MSG_PRE_APPROVAL_REQUEST:
                    handlePreapprovalRequest();
                    break;
            }

            return true;
@@ -779,10 +795,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
     */
    private boolean isInstallerDeviceOwnerOrAffiliatedProfileOwner() {
        assertNotLocked("isInstallerDeviceOwnerOrAffiliatedProfileOwner");
        // It is safe to access mInstallerUid and mInstallSource without lock
        // because they are immutable after sealing.
        assertSealed("isInstallerDeviceOwnerOrAffiliatedProfileOwner");
        if (userId != UserHandle.getUserId(mInstallerUid)) {
        if (userId != UserHandle.getUserId(getInstallerUid())) {
            return false;
        }
        DevicePolicyManagerInternal dpmi =
@@ -1032,17 +1045,21 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
            info.progress = progress;
            info.sealed = mSealed;
            info.isCommitted = mCommitted.get();
            info.isPreapprovalRequested = isPreapprovalRequested();
            info.active = mActiveCount.get() > 0;

            info.mode = params.mode;
            info.installReason = params.installReason;
            info.installScenario = params.installScenario;
            info.sizeBytes = params.sizeBytes;
            info.appPackageName = mPackageName != null ? mPackageName : params.appPackageName;
            info.appPackageName = mPreapprovalDetails != null ? mPreapprovalDetails.getPackageName()
                    : mPackageName != null ? mPackageName : params.appPackageName;
            if (includeIcon) {
                info.appIcon = params.appIcon;
                info.appIcon = mPreapprovalDetails != null && mPreapprovalDetails.getIcon() != null
                        ? mPreapprovalDetails.getIcon() : params.appIcon;
            }
            info.appLabel = params.appLabel;
            info.appLabel =
                    mPreapprovalDetails != null ? mPreapprovalDetails.getLabel() : params.appLabel;

            info.installLocation = params.installLocation;
            if (!scrubData) {
@@ -1086,6 +1103,11 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
        }
    }

    /** @hide */
    boolean isPreapprovalRequested() {
        return mPreapprovalRequested.get();
    }

    /** {@hide} */
    boolean isCommitted() {
        return mCommitted.get();
@@ -1121,6 +1143,14 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
        }
    }

    @GuardedBy("mLock")
    private void assertPreparedAndNotPreapprovalRequestedLocked(String cookie) {
        assertPreparedAndNotSealedLocked(cookie);
        if (isPreapprovalRequested()) {
            throw new IllegalStateException(cookie + " not allowed after requesting");
        }
    }

    @GuardedBy("mLock")
    private void assertPreparedAndNotSealedLocked(String cookie) {
        assertPreparedAndNotCommittedOrDestroyedLocked(cookie);
@@ -1708,6 +1738,20 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
        }
    }

    @WorkerThread
    private void handlePreapprovalRequest() {
        /**
         * Stops the process if the session needs user action. When the user answers the yes,
         * {@link #setPermissionsResult(boolean)} is called and then
         * {@link #MSG_PRE_APPROVAL_REQUEST} is handled to come back here to check again.
         */
        if (sendPendingUserActionIntentIfNeeded()) {
            return;
        }

        dispatchSessionPreappoved();
    }

    private final class FileSystemConnector extends
            IPackageInstallerSessionFileSystemConnector.Stub {
        final Set<String> mAddedFiles = new ArraySet<>();
@@ -2115,7 +2159,11 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
     */
    @WorkerThread
    private boolean sendPendingUserActionIntentIfNeeded() {
        // To support pre-approval request of atomic install, we allow child session to handle
        // the result by itself since it has the status receiver.
        if (isCommitted()) {
            assertNotChild("PackageInstallerSession#sendPendingUserActionIntentIfNeeded");
        }

        final IntentSender statusReceiver = getRemoteStatusReceiver();
        return sessionContains(s -> checkUserActionRequirement(s, statusReceiver));
@@ -2420,7 +2468,10 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
        // User needs to confirm installation;
        // give installer an intent they can use to involve
        // user.
        final Intent intent = new Intent(PackageInstaller.ACTION_CONFIRM_INSTALL);
        final boolean isPreapproval = isPreapprovalRequested() && !isCommitted();
        final Intent intent = new Intent(
                isPreapproval ? PackageInstaller.ACTION_CONFIRM_PRE_APPROVAL
                        : PackageInstaller.ACTION_CONFIRM_INSTALL);
        intent.setPackage(mPm.getPackageInstallerPackageName());
        intent.putExtra(PackageInstaller.EXTRA_SESSION_ID, sessionId);

@@ -3009,6 +3060,9 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
                }
            }
        }

        assertPreapprovalDetailsConsistentIfNeededLocked(packageLite, pkgInfo);

        if (packageLite.isUseEmbeddedDex()) {
            for (File file : mResolvedStagedFiles) {
                if (file.getName().endsWith(".apk")
@@ -3253,6 +3307,78 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
        }
    }

    @GuardedBy("mLock")
    private void assertPreapprovalDetailsConsistentIfNeededLocked(@NonNull PackageLite packageLite,
            @Nullable PackageInfo info) throws PackageManagerException {
        if (mPreapprovalDetails == null || !isPreapprovalRequested()) {
            return;
        }

        if (!TextUtils.equals(mPackageName, mPreapprovalDetails.getPackageName())) {
            throw new PackageManagerException(INSTALL_FAILED_INTERNAL_ERROR,
                    mPreapprovalDetails + " inconsistent with " + mPackageName);
        }

        // In case the app label in PreapprovalDetails from different locale in split APK,
        // we check all APK files to find the app label.
        final PackageInfo packageInfo =
                info != null ? info : mContext.getPackageManager().getPackageArchiveInfo(
                        packageLite.getPath(), PackageInfoFlags.of(0));
        if (packageInfo == null) {
            throw new PackageManagerException(INSTALL_FAILED_INVALID_APK,
                    "Failure to obtain package info.");
        }
        final List<String> filePaths = packageLite.getAllApkPaths();
        final String appLabel = mPreapprovalDetails.getLabel();
        final ULocale appLocale = mPreapprovalDetails.getLocale();
        final ApplicationInfo appInfo = packageInfo.applicationInfo;
        boolean appLabelMatched = false;
        for (int i = filePaths.size() - 1; i >= 0 && !appLabelMatched; i--) {
            appLabelMatched |= TextUtils.equals(getAppLabel(filePaths.get(i), appLocale, appInfo),
                    appLabel);
        }
        if (!appLabelMatched) {
            throw new PackageManagerException(INSTALL_FAILED_INTERNAL_ERROR,
                    mPreapprovalDetails + " inconsistent with app label");
        }
    }

    private CharSequence getAppLabel(String path, ULocale locale, ApplicationInfo appInfo)
            throws PackageManagerException {
        final Resources pRes = mContext.getResources();
        final AssetManager assetManager = new AssetManager();
        final Configuration config = new Configuration(pRes.getConfiguration());
        final ApkAssets apkAssets;
        try {
            apkAssets = ApkAssets.loadFromPath(path);
        } catch (IOException e) {
            throw new PackageManagerException(INSTALL_FAILED_INVALID_APK,
                    "Failure to get resources from package archive " + path);
        }
        assetManager.setApkAssets(new ApkAssets[]{apkAssets}, false /* invalidateCaches */);
        config.setLocale(locale.toLocale());
        final Resources res = new Resources(assetManager, pRes.getDisplayMetrics(), config);
        return tryLoadingAppLabel(res, appInfo);
    }

    private CharSequence tryLoadingAppLabel(@NonNull Resources res, @NonNull ApplicationInfo info) {
        CharSequence label = null;
        // Try to load the label from the package's resources. If an app has not explicitly
        // specified any label, just use the package name.
        if (info.labelRes != 0) {
            try {
                label = res.getText(info.labelRes);
            } catch (Resources.NotFoundException ignore) {
            }
        }
        if (label == null) {
            label = (info.nonLocalizedLabel != null)
                    ? info.nonLocalizedLabel : info.packageName;
        }

        return label;
    }

    private SigningDetails unsafeGetCertsWithoutVerification(String path)
            throws PackageManagerException {
        final ParseTypeImpl input = ParseTypeImpl.forDefaultParsing();
@@ -3469,11 +3595,13 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
    }

    void setPermissionsResult(boolean accepted) {
        if (!isSealed()) {
        if (!isSealed() && !isPreapprovalRequested()) {
            throw new SecurityException("Must be sealed to accept permissions");
        }

        PackageInstallerSession root = hasParentSessionId()
        // To support pre-approval request of atomic install, we allow child session to handle
        // the result by itself since it has the status receiver.
        final PackageInstallerSession root = hasParentSessionId() && isCommitted()
                ? mSessionProvider.getSession(getParentSessionId()) : this;

        if (accepted) {
@@ -3481,7 +3609,8 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
            synchronized (mLock) {
                mPermissionsManuallyAccepted = true;
            }
            root.mHandler.obtainMessage(MSG_INSTALL).sendToTarget();
            root.mHandler.obtainMessage(
                    isCommitted() ? MSG_INSTALL : MSG_PRE_APPROVAL_REQUEST).sendToTarget();
        } else {
            root.destroy();
            root.dispatchSessionFinished(INSTALL_FAILED_ABORTED, "User rejected permissions", null);
@@ -4096,10 +4225,68 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
            args.arg3 = extras;
            args.arg4 = statusReceiver;
            args.argi1 = returnCode;
            args.argi2 = isPreapprovalRequested() && !isCommitted() ? 1 : 0;
            mHandler.obtainMessage(MSG_ON_PACKAGE_INSTALLED, args).sendToTarget();
        }
    }

    private void dispatchSessionPreappoved() {
        final IntentSender target = getRemoteStatusReceiver();
        final Intent intent = new Intent();
        intent.putExtra(PackageInstaller.EXTRA_SESSION_ID, sessionId);
        intent.putExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_SUCCESS);
        intent.putExtra(PackageInstaller.EXTRA_PRE_APPROVAL, true);
        try {
            target.sendIntent(mContext, 0 /* code */, intent, null /* onFinished */,
                    null /* handler */);
        } catch (IntentSender.SendIntentException ignored) {
        }
    }

    @Override
    public void requestUserPreapproval(@NonNull PreapprovalDetails details,
            @NonNull IntentSender statusReceiver) {
        validatePreapprovalRequest(details, statusReceiver);
        dispatchPreapprovalRequest();
    }

    /**
     * Validates whether the necessary information (e.g., PreapprovalDetails) are provided.
     */
    private void validatePreapprovalRequest(@NonNull PreapprovalDetails details,
            @NonNull IntentSender statusReceiver) {
        assertCallerIsOwnerOrRoot();
        if (isMultiPackage()) {
            throw new IllegalStateException(
                    "Session " + sessionId + " is a parent of multi-package session and "
                            + "requestUserPreapproval on the parent session isn't supported.");
        }

        synchronized (mLock) {
            assertPreparedAndNotSealedLocked("request of session " + sessionId);
            mPreapprovalDetails = details;
            setRemoteStatusReceiver(statusReceiver);
        }
    }

    private void dispatchPreapprovalRequest() {
        synchronized (mLock) {
            assertPreparedAndNotPreapprovalRequestedLocked("dispatchPreapprovalRequest");
        }

        // Mark this session are pre-approval requested, and ready to progress to the next phase.
        markAsPreapprovalRequested();

        mHandler.obtainMessage(MSG_PRE_APPROVAL_REQUEST).sendToTarget();
    }

    /**
     * Marks this session as pre-approval requested, and prevents further related modification.
     */
    private void markAsPreapprovalRequested() {
        mPreapprovalRequested.set(true);
    }

    void setSessionReady() {
        synchronized (mLock) {
            // Do not allow destroyed/failed session to change state
@@ -4265,6 +4452,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
        pw.printPair("mClientProgress", clientProgress);
        pw.printPair("mProgress", progress);
        pw.printPair("mCommitted", mCommitted);
        pw.printPair("mPreapprovalRequested", mPreapprovalRequested);
        pw.printPair("mSealed", mSealed);
        pw.printPair("mPermissionsManuallyAccepted", mPermissionsManuallyAccepted);
        pw.printPair("mStageDirInUse", mStageDirInUse);
@@ -4282,6 +4470,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
        pw.printPair("mSessionReady", mSessionReady);
        pw.printPair("mSessionErrorCode", mSessionErrorCode);
        pw.printPair("mSessionErrorMessage", mSessionErrorMessage);
        pw.printPair("mPreapprovalDetails", mPreapprovalDetails);
        pw.println();

        pw.decreaseIndent();
@@ -4295,6 +4484,8 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
        final Intent fillIn = new Intent();
        fillIn.putExtra(PackageInstaller.EXTRA_SESSION_ID, sessionId);
        fillIn.putExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_PENDING_USER_ACTION);
        fillIn.putExtra(PackageInstaller.EXTRA_PRE_APPROVAL,
                PackageInstaller.ACTION_CONFIRM_PRE_APPROVAL.equals(intent.getAction()));
        fillIn.putExtra(Intent.EXTRA_INTENT, intent);
        try {
            target.sendIntent(context, 0, fillIn, null, null);
@@ -4307,7 +4498,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
     */
    private static void sendOnPackageInstalled(Context context, IntentSender target, int sessionId,
            boolean showNotification, int userId, String basePackageName, int returnCode,
            String msg, Bundle extras) {
            boolean isPreapproval, String msg, Bundle extras) {
        if (INSTALL_SUCCEEDED == returnCode && showNotification) {
            boolean update = (extras != null) && extras.getBoolean(Intent.EXTRA_REPLACING);
            Notification notification = PackageInstallerService.buildSuccessNotification(context,
@@ -4330,6 +4521,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
        fillIn.putExtra(PackageInstaller.EXTRA_STATUS_MESSAGE,
                PackageManager.installStatusToString(returnCode, msg));
        fillIn.putExtra(PackageInstaller.EXTRA_LEGACY_STATUS, returnCode);
        fillIn.putExtra(PackageInstaller.EXTRA_PRE_APPROVAL, isPreapproval);
        if (extras != null) {
            final String existing = extras.getString(
                    PackageManager.EXTRA_FAILURE_EXISTING_PACKAGE);
+1 −0
Original line number Diff line number Diff line
@@ -325,6 +325,7 @@ public class PackageInstallerSessionTest {
                actual.getSessionErrorMessage());
        assertEquals(expected.isPrepared(), actual.isPrepared());
        assertEquals(expected.isCommitted(), actual.isCommitted());
        assertEquals(expected.isPreapprovalRequested(), actual.isPreapprovalRequested());
        assertEquals(expected.createdMillis, actual.createdMillis);
        assertEquals(expected.isSealed(), actual.isSealed());
        assertEquals(expected.isMultiPackage(), actual.isMultiPackage());