From b0ff86efa9f603d41b5cc97eb746246202d0132b Mon Sep 17 00:00:00 2001 From: Jackal Guo Date: Wed, 24 Aug 2022 16:55:15 +0800 Subject: [PATCH] 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 --- core/api/current.txt | 2 + .../content/pm/IPackageInstallerSession.aidl | 3 + .../android/content/pm/PackageInstaller.java | 55 +++++ .../server/pm/PackageInstallerSession.java | 220 ++++++++++++++++-- .../pm/PackageInstallerSessionTest.java | 1 + 5 files changed, 267 insertions(+), 14 deletions(-) diff --git a/core/api/current.txt b/core/api/current.txt index 27d5ac3efa425..935a3a559f5d9 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -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, @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, @Nullable byte[]) throws java.io.IOException; method public void setStagingProgress(float); method public void transfer(@NonNull String) throws android.content.pm.PackageManager.NameNotFoundException; diff --git a/core/java/android/content/pm/IPackageInstallerSession.aidl b/core/java/android/content/pm/IPackageInstallerSession.aidl index 8d6c8e8d9f629..1fc6bdaa963a9 100644 --- a/core/java/android/content/pm/IPackageInstallerSession.aidl +++ b/core/java/android/content/pm/IPackageInstallerSession.aidl @@ -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); } diff --git a/core/java/android/content/pm/PackageInstaller.java b/core/java/android/content/pm/PackageInstaller.java index 5b182730b2842..55222a3be768d 100644 --- a/core/java/android/content/pm/PackageInstaller.java +++ b/core/java/android/content/pm/PackageInstaller.java @@ -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); diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java index e9f26e99cb4ea..f560ab01d2720 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerSession.java +++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java @@ -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 mAddedFiles = new ArraySet<>(); @@ -2115,7 +2159,11 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { */ @WorkerThread private boolean sendPendingUserActionIntentIfNeeded() { - assertNotChild("PackageInstallerSession#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 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); diff --git a/services/tests/servicestests/src/com/android/server/pm/PackageInstallerSessionTest.java b/services/tests/servicestests/src/com/android/server/pm/PackageInstallerSessionTest.java index 27c3ca46cb207..a545b1fd4e150 100644 --- a/services/tests/servicestests/src/com/android/server/pm/PackageInstallerSessionTest.java +++ b/services/tests/servicestests/src/com/android/server/pm/PackageInstallerSessionTest.java @@ -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());