Add an intentSender to the unarchival API and add onUnarchivalStatus API
with failure handling.

Also add some missing permission checks.

Bug: 308150552
Test: PackageInstallerArchiveTest
Change-Id: I755fe01fd25d5cc405fb30be62e6a9b94ea1d3a3
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 32d252e..275fe77 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -3868,8 +3868,9 @@
   public class PackageInstaller {
     method @NonNull public android.content.pm.PackageInstaller.InstallInfo readInstallInfo(@NonNull java.io.File, int) throws android.content.pm.PackageInstaller.PackageParsingException;
     method @NonNull public android.content.pm.PackageInstaller.InstallInfo readInstallInfo(@NonNull android.os.ParcelFileDescriptor, @Nullable String, int) throws android.content.pm.PackageInstaller.PackageParsingException;
+    method @FlaggedApi("android.content.pm.archiving") @RequiresPermission(anyOf={android.Manifest.permission.INSTALL_PACKAGES, android.Manifest.permission.REQUEST_INSTALL_PACKAGES}) public void reportUnarchivalStatus(int, int, long, @Nullable android.app.PendingIntent) throws android.content.pm.PackageManager.NameNotFoundException;
     method @FlaggedApi("android.content.pm.archiving") @RequiresPermission(anyOf={android.Manifest.permission.DELETE_PACKAGES, android.Manifest.permission.REQUEST_DELETE_PACKAGES}) public void requestArchive(@NonNull String, @NonNull android.content.IntentSender) throws android.content.pm.PackageManager.NameNotFoundException;
-    method @FlaggedApi("android.content.pm.archiving") @RequiresPermission(anyOf={android.Manifest.permission.INSTALL_PACKAGES, android.Manifest.permission.REQUEST_INSTALL_PACKAGES}) public void requestUnarchive(@NonNull String) throws java.io.IOException, android.content.pm.PackageManager.NameNotFoundException;
+    method @FlaggedApi("android.content.pm.archiving") @RequiresPermission(anyOf={android.Manifest.permission.INSTALL_PACKAGES, android.Manifest.permission.REQUEST_INSTALL_PACKAGES}) public void requestUnarchive(@NonNull String, @NonNull android.content.IntentSender) throws java.io.IOException, android.content.pm.PackageManager.NameNotFoundException;
     method @RequiresPermission(android.Manifest.permission.INSTALL_PACKAGES) public void setPermissionsResult(int, boolean);
     field public static final String ACTION_CONFIRM_INSTALL = "android.content.pm.action.CONFIRM_INSTALL";
     field public static final String ACTION_CONFIRM_PRE_APPROVAL = "android.content.pm.action.CONFIRM_PRE_APPROVAL";
@@ -3883,12 +3884,20 @@
     field @FlaggedApi("android.content.pm.archiving") public static final String EXTRA_UNARCHIVE_ALL_USERS = "android.content.pm.extra.UNARCHIVE_ALL_USERS";
     field @FlaggedApi("android.content.pm.archiving") public static final String EXTRA_UNARCHIVE_ID = "android.content.pm.extra.UNARCHIVE_ID";
     field @FlaggedApi("android.content.pm.archiving") public static final String EXTRA_UNARCHIVE_PACKAGE_NAME = "android.content.pm.extra.UNARCHIVE_PACKAGE_NAME";
+    field @FlaggedApi("android.content.pm.archiving") public static final String EXTRA_UNARCHIVE_STATUS = "android.content.pm.extra.UNARCHIVE_STATUS";
     field public static final int LOCATION_DATA_APP = 0; // 0x0
     field public static final int LOCATION_MEDIA_DATA = 2; // 0x2
     field public static final int LOCATION_MEDIA_OBB = 1; // 0x1
     field public static final int REASON_CONFIRM_PACKAGE_CHANGE = 0; // 0x0
     field public static final int REASON_OWNERSHIP_CHANGED = 1; // 0x1
     field public static final int REASON_REMIND_OWNERSHIP = 2; // 0x2
+    field @FlaggedApi("android.content.pm.archiving") public static final int UNARCHIVAL_ERROR_INSTALLER_DISABLED = 4; // 0x4
+    field @FlaggedApi("android.content.pm.archiving") public static final int UNARCHIVAL_ERROR_INSTALLER_UNINSTALLED = 5; // 0x5
+    field @FlaggedApi("android.content.pm.archiving") public static final int UNARCHIVAL_ERROR_INSUFFICIENT_STORAGE = 2; // 0x2
+    field @FlaggedApi("android.content.pm.archiving") public static final int UNARCHIVAL_ERROR_NO_CONNECTIVITY = 3; // 0x3
+    field @FlaggedApi("android.content.pm.archiving") public static final int UNARCHIVAL_ERROR_USER_ACTION_NEEDED = 1; // 0x1
+    field @FlaggedApi("android.content.pm.archiving") public static final int UNARCHIVAL_GENERIC_ERROR = 100; // 0x64
+    field @FlaggedApi("android.content.pm.archiving") public static final int UNARCHIVAL_OK = 0; // 0x0
   }
 
   public static class PackageInstaller.InstallInfo {
diff --git a/core/java/android/content/pm/IPackageInstaller.aidl b/core/java/android/content/pm/IPackageInstaller.aidl
index 59ed045..1f25fd0 100644
--- a/core/java/android/content/pm/IPackageInstaller.aidl
+++ b/core/java/android/content/pm/IPackageInstaller.aidl
@@ -16,6 +16,7 @@
 
 package android.content.pm;
 
+import android.app.PendingIntent;
 import android.content.pm.ArchivedPackageParcel;
 import android.content.pm.IPackageDeleteObserver2;
 import android.content.pm.IPackageInstallerCallback;
@@ -82,7 +83,7 @@
     void requestArchive(String packageName, String callerPackageName, in IntentSender statusReceiver, in UserHandle userHandle);
 
     @JavaPassthrough(annotation="@android.annotation.RequiresPermission(anyOf={android.Manifest.permission.INSTALL_PACKAGES,android.Manifest.permission.REQUEST_INSTALL_PACKAGES})")
-    void requestUnarchive(String packageName, String callerPackageName, in UserHandle userHandle);
+    void requestUnarchive(String packageName, String callerPackageName, in IntentSender statusReceiver, in UserHandle userHandle);
 
     @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.INSTALL_PACKAGES)")
     void installPackageArchived(in ArchivedPackageParcel archivedPackageParcel,
@@ -90,4 +91,6 @@
             in IntentSender statusReceiver,
             String installerPackageName, in UserHandle userHandle);
 
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(anyOf={android.Manifest.permission.INSTALL_PACKAGES,android.Manifest.permission.REQUEST_INSTALL_PACKAGES})")
+    void reportUnarchivalStatus(int unarchiveId, int status, long requiredStorageBytes, in PendingIntent userActionIntent, in UserHandle userHandle);
 }
diff --git a/core/java/android/content/pm/PackageInstaller.java b/core/java/android/content/pm/PackageInstaller.java
index e9a2aaa..4f0bfc7 100644
--- a/core/java/android/content/pm/PackageInstaller.java
+++ b/core/java/android/content/pm/PackageInstaller.java
@@ -387,6 +387,24 @@
             "android.content.pm.extra.UNARCHIVE_ALL_USERS";
 
     /**
+     * Current status of an unarchive operation. Will be one of
+     * {@link #UNARCHIVAL_OK}, {@link #UNARCHIVAL_ERROR_USER_ACTION_NEEDED},
+     * {@link #UNARCHIVAL_ERROR_INSUFFICIENT_STORAGE}, {@link #UNARCHIVAL_ERROR_NO_CONNECTIVITY},
+     * {@link #UNARCHIVAL_GENERIC_ERROR}, {@link #UNARCHIVAL_ERROR_INSTALLER_DISABLED} or
+     * {@link #UNARCHIVAL_ERROR_INSTALLER_UNINSTALLED}.
+     *
+     * <p> If the status is not {@link #UNARCHIVAL_OK}, then {@link Intent#EXTRA_INTENT} will be set
+     * with an intent for a corresponding follow-up action (e.g. storage clearing dialog) or a
+     * failure dialog.
+     *
+     * @see #requestUnarchive
+     * @hide
+     */
+    @SystemApi
+    @FlaggedApi(Flags.FLAG_ARCHIVING)
+    public static final String EXTRA_UNARCHIVE_STATUS = "android.content.pm.extra.UNARCHIVE_STATUS";
+
+    /**
      * A list of warnings that occurred during installation.
      *
      * @hide
@@ -652,6 +670,102 @@
     @Retention(RetentionPolicy.SOURCE)
     public @interface UserActionReason {}
 
+    /**
+     * The unarchival is possible and will commence.
+     *
+     * <p> Note that this does not mean that the unarchival has completed. This status should be
+     * sent before any longer asynchronous action (e.g. app download) is started.
+     *
+     * @hide
+     */
+    @SystemApi
+    @FlaggedApi(Flags.FLAG_ARCHIVING)
+    public static final int UNARCHIVAL_OK = 0;
+
+    /**
+     * The user needs to interact with the installer to enable the installation.
+     *
+     * <p> An example use case for this could be that the user needs to login to allow the
+     * download for a paid app.
+     *
+     * @hide
+     */
+    @SystemApi
+    @FlaggedApi(Flags.FLAG_ARCHIVING)
+    public static final int UNARCHIVAL_ERROR_USER_ACTION_NEEDED = 1;
+
+    /**
+     * Not enough storage to unarchive the application.
+     *
+     * <p> The installer can optionally provide a {@code userActionIntent} for a space-clearing
+     * dialog. If no action is provided, then a generic intent
+     * {@link android.os.storage.StorageManager#ACTION_MANAGE_STORAGE} is started instead.
+     *
+     * @hide
+     */
+    @SystemApi
+    @FlaggedApi(Flags.FLAG_ARCHIVING)
+    public static final int UNARCHIVAL_ERROR_INSUFFICIENT_STORAGE = 2;
+
+    /**
+     * The device is not connected to the internet
+     *
+     * @hide
+     */
+    @SystemApi
+    @FlaggedApi(Flags.FLAG_ARCHIVING)
+    public static final int UNARCHIVAL_ERROR_NO_CONNECTIVITY = 3;
+
+    /**
+     * The installer responsible for the unarchival is disabled.
+     *
+     * <p> Should only be used by the system.
+     *
+     * @hide
+     */
+    @SystemApi
+    @FlaggedApi(Flags.FLAG_ARCHIVING)
+    public static final int UNARCHIVAL_ERROR_INSTALLER_DISABLED = 4;
+
+    /**
+     * The installer responsible for the unarchival has been uninstalled
+     *
+     * <p> Should only be used by the system.
+     *
+     * @hide
+     */
+    @SystemApi
+    @FlaggedApi(Flags.FLAG_ARCHIVING)
+    public static final int UNARCHIVAL_ERROR_INSTALLER_UNINSTALLED = 5;
+
+    /**
+     * Generic error: The app cannot be unarchived.
+     *
+     * @hide
+     */
+    @SystemApi
+    @FlaggedApi(Flags.FLAG_ARCHIVING)
+    public static final int UNARCHIVAL_GENERIC_ERROR = 100;
+
+    /**
+     * The set of error types that can be set for
+     * {@link #reportUnarchivalStatus(int, int, PendingIntent)}.
+     *
+     * @hide
+     */
+    @IntDef(value = {
+            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,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface UnarchivalStatus {}
+
+
     /** Default set of checksums - includes all available checksums.
      * @see Session#requestChecksums  */
     private static final int DEFAULT_CHECKSUMS =
@@ -2238,8 +2352,8 @@
      * Requests to archive a package which is currently installed.
      *
      * <p> During the archival process, the apps APKs and cache are removed from the device while
-     * the user data is kept. Through the {@link #requestUnarchive(String)} call, apps can be
-     * restored again through their responsible installer.
+     * the user data is kept. Through the {@link #requestUnarchive} call, apps
+     * can be restored again through their responsible installer.
      *
      * <p> Archived apps are returned as displayable apps through the {@link LauncherApps} APIs and
      * will be displayed to users with UI treatment to highlight that said apps are archived. If
@@ -2278,6 +2392,10 @@
      * <p> The installation will happen asynchronously and can be observed through
      * {@link android.content.Intent#ACTION_PACKAGE_ADDED}.
      *
+     * @param statusReceiver Callback used to notify whether the installer has accepted the
+     *                       unarchival request or an error has occurred. The status update will be
+     *                       sent though {@link EXTRA_UNARCHIVE_STATUS}. Only one status will be
+     *                       sent.
      * @throws PackageManager.NameNotFoundException If {@code packageName} isn't found or not
      *                                              visible to the caller or if the package has no
      *                                              installer on the device anymore to unarchive it.
@@ -2290,10 +2408,10 @@
             Manifest.permission.REQUEST_INSTALL_PACKAGES})
     @SystemApi
     @FlaggedApi(Flags.FLAG_ARCHIVING)
-    public void requestUnarchive(@NonNull String packageName)
+    public void requestUnarchive(@NonNull String packageName, @NonNull IntentSender statusReceiver)
             throws IOException, PackageManager.NameNotFoundException {
         try {
-            mInstaller.requestUnarchive(packageName, mInstallerPackageName,
+            mInstaller.requestUnarchive(packageName, mInstallerPackageName, statusReceiver,
                     new UserHandle(mUserId));
         } catch (ParcelableException e) {
             e.maybeRethrow(IOException.class);
@@ -2303,6 +2421,39 @@
         }
     }
 
+    /**
+     * Reports the status of an unarchival to the system.
+     *
+     * @param unarchiveId          the ID provided by the system as part of the
+     *                             intent.action.UNARCHIVE broadcast with EXTRA_UNARCHIVE_ID.
+     * @param status               is used for the system to provide the user with necessary
+     *                             follow-up steps or errors.
+     * @param requiredStorageBytes If the error is UNARCHIVAL_ERROR_INSUFFICIENT_STORAGE this field
+     *                             should be set to specify how many additional bytes of storage
+     *                             are required to unarchive the app.
+     * @param userActionIntent     Optional intent to start a follow up action required to
+     *                             facilitate the unarchival flow (e.g. user needs to log in).
+     * @throws PackageManager.NameNotFoundException if no unarchival with {@code unarchiveId} exists
+     * @hide
+     */
+    @RequiresPermission(anyOf = {
+            Manifest.permission.INSTALL_PACKAGES,
+            Manifest.permission.REQUEST_INSTALL_PACKAGES})
+    @SystemApi
+    @FlaggedApi(Flags.FLAG_ARCHIVING)
+    public void reportUnarchivalStatus(int unarchiveId, @UnarchivalStatus int status,
+            long requiredStorageBytes, @Nullable PendingIntent userActionIntent)
+            throws PackageManager.NameNotFoundException {
+        try {
+            mInstaller.reportUnarchivalStatus(unarchiveId, status, requiredStorageBytes,
+                    userActionIntent, new UserHandle(mUserId));
+        } catch (ParcelableException e) {
+            e.maybeRethrow(PackageManager.NameNotFoundException.class);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
     // (b/239722738) This class serves as a bridge between the PackageLite class, which
     // is a hidden class, and the consumers of this class. (e.g. InstallInstalling.java)
     // This is a part of an effort to remove dependency on hidden APIs and use SystemAPIs or
@@ -2566,6 +2717,8 @@
         public int developmentInstallFlags = 0;
         /** {@hide} */
         public int unarchiveId = -1;
+        /** {@hide} */
+        public IntentSender unarchiveIntentSender;
 
         private final ArrayMap<String, Integer> mPermissionStates;
 
@@ -2618,6 +2771,7 @@
             applicationEnabledSettingPersistent = source.readBoolean();
             developmentInstallFlags = source.readInt();
             unarchiveId = source.readInt();
+            unarchiveIntentSender = source.readParcelable(null, IntentSender.class);
         }
 
         /** {@hide} */
@@ -2652,6 +2806,7 @@
             ret.applicationEnabledSettingPersistent = applicationEnabledSettingPersistent;
             ret.developmentInstallFlags = developmentInstallFlags;
             ret.unarchiveId = unarchiveId;
+            ret.unarchiveIntentSender = unarchiveIntentSender;
             return ret;
         }
 
@@ -3364,6 +3519,7 @@
                     applicationEnabledSettingPersistent);
             pw.printHexPair("developmentInstallFlags", developmentInstallFlags);
             pw.printPair("unarchiveId", unarchiveId);
+            pw.printPair("unarchiveIntentSender", unarchiveIntentSender);
             pw.println();
         }
 
@@ -3408,6 +3564,7 @@
             dest.writeBoolean(applicationEnabledSettingPersistent);
             dest.writeInt(developmentInstallFlags);
             dest.writeInt(unarchiveId);
+            dest.writeParcelable(unarchiveIntentSender, flags);
         }
 
         public static final Parcelable.Creator<SessionParams>
diff --git a/services/core/java/com/android/server/pm/PackageArchiver.java b/services/core/java/com/android/server/pm/PackageArchiver.java
index eff6157..1a70222 100644
--- a/services/core/java/com/android/server/pm/PackageArchiver.java
+++ b/services/core/java/com/android/server/pm/PackageArchiver.java
@@ -140,6 +140,8 @@
         }
         snapshot.enforceCrossUserPermission(binderUid, userId, true, true,
                 "archiveApp");
+        verifyUninstallPermissions();
+
         CompletableFuture<ArchiveState> archiveStateFuture;
         try {
             archiveStateFuture = createArchiveState(packageName, userId);
@@ -372,9 +374,11 @@
     void requestUnarchive(
             @NonNull String packageName,
             @NonNull String callerPackageName,
+            @NonNull IntentSender statusReceiver,
             @NonNull UserHandle userHandle) {
         Objects.requireNonNull(packageName);
         Objects.requireNonNull(callerPackageName);
+        Objects.requireNonNull(statusReceiver);
         Objects.requireNonNull(userHandle);
 
         Computer snapshot = mPm.snapshotComputer();
@@ -385,6 +389,8 @@
         }
         snapshot.enforceCrossUserPermission(binderUid, userId, true, true,
                 "unarchiveApp");
+        verifyInstallPermissions();
+
         PackageStateInternal ps;
         try {
             ps = getPackageState(packageName, snapshot, binderUid, userId);
@@ -400,9 +406,12 @@
                                     packageName)));
         }
 
+        // TODO(b/305902395) Introduce a confirmation dialog if the requestor only holds
+        // REQUEST_INSTALL permission.
         int draftSessionId;
         try {
-            draftSessionId = createDraftSession(packageName, installerPackage, userId);
+            draftSessionId = createDraftSession(packageName, installerPackage, statusReceiver,
+                    userId);
         } catch (RuntimeException e) {
             if (e.getCause() instanceof IOException) {
                 throw ExceptionUtils.wrap((IOException) e.getCause());
@@ -415,11 +424,36 @@
                 () -> unarchiveInternal(packageName, userHandle, installerPackage, draftSessionId));
     }
 
-    private int createDraftSession(String packageName, String installerPackage, int userId) {
+    private void verifyInstallPermissions() {
+        if (mContext.checkCallingOrSelfPermission(Manifest.permission.INSTALL_PACKAGES)
+                != PackageManager.PERMISSION_GRANTED && mContext.checkCallingOrSelfPermission(
+                Manifest.permission.REQUEST_INSTALL_PACKAGES)
+                != PackageManager.PERMISSION_GRANTED) {
+            throw new SecurityException("You need the com.android.permission.INSTALL_PACKAGES "
+                    + "or com.android.permission.REQUEST_INSTALL_PACKAGES permission to request "
+                    + "an unarchival.");
+        }
+    }
+
+    private void verifyUninstallPermissions() {
+        if (mContext.checkCallingOrSelfPermission(Manifest.permission.DELETE_PACKAGES)
+                != PackageManager.PERMISSION_GRANTED && mContext.checkCallingOrSelfPermission(
+                Manifest.permission.REQUEST_DELETE_PACKAGES)
+                != PackageManager.PERMISSION_GRANTED) {
+            throw new SecurityException("You need the com.android.permission.DELETE_PACKAGES "
+                    + "or com.android.permission.REQUEST_DELETE_PACKAGES permission to request "
+                    + "an archival.");
+        }
+    }
+
+    private int createDraftSession(String packageName, String installerPackage,
+            IntentSender statusReceiver, int userId) {
         PackageInstaller.SessionParams sessionParams = new PackageInstaller.SessionParams(
                 PackageInstaller.SessionParams.MODE_FULL_INSTALL);
         sessionParams.setAppPackageName(packageName);
         sessionParams.installFlags = INSTALL_UNARCHIVE_DRAFT;
+        sessionParams.unarchiveIntentSender = statusReceiver;
+
         int installerUid = mPm.snapshotComputer().getPackageUid(installerPackage, 0, userId);
         // Handles case of repeated unarchival calls for the same package.
         int existingSessionId = mPm.mInstallerService.getExistingDraftSessionId(installerUid,
diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java
index af43a8b..c9663fc 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerService.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerService.java
@@ -16,8 +16,14 @@
 
 package com.android.server.pm;
 
+import static android.app.ComponentOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED;
 import static android.app.admin.DevicePolicyResources.Strings.Core.PACKAGE_DELETED_BY_DO;
 import static android.content.pm.PackageInstaller.LOCATION_DATA_APP;
+import static android.content.pm.PackageInstaller.UNARCHIVAL_ERROR_INSUFFICIENT_STORAGE;
+import static android.content.pm.PackageInstaller.UNARCHIVAL_ERROR_NO_CONNECTIVITY;
+import static android.content.pm.PackageInstaller.UNARCHIVAL_ERROR_USER_ACTION_NEEDED;
+import static android.content.pm.PackageInstaller.UNARCHIVAL_GENERIC_ERROR;
+import static android.content.pm.PackageInstaller.UNARCHIVAL_OK;
 import static android.content.pm.PackageManager.INSTALL_UNARCHIVE_DRAFT;
 import static android.os.Process.INVALID_UID;
 import static android.os.Process.SYSTEM_UID;
@@ -37,6 +43,7 @@
 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;
@@ -56,6 +63,7 @@
 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.ParceledListSlice;
@@ -71,6 +79,7 @@
 import android.os.HandlerThread;
 import android.os.Looper;
 import android.os.Message;
+import android.os.ParcelableException;
 import android.os.Process;
 import android.os.RemoteCallback;
 import android.os.RemoteCallbackList;
@@ -1630,8 +1639,10 @@
     public void requestUnarchive(
             @NonNull String packageName,
             @NonNull String callerPackageName,
+            @NonNull IntentSender statusReceiver,
             @NonNull UserHandle userHandle) {
-        mPackageArchiver.requestUnarchive(packageName, callerPackageName, userHandle);
+        mPackageArchiver.requestUnarchive(packageName, callerPackageName, statusReceiver,
+                userHandle);
     }
 
     @Override
@@ -1689,6 +1700,102 @@
         }
     }
 
+    // TODO(b/307299702) Implement error dialog and propagate userActionIntent.
+    @Override
+    public void reportUnarchivalStatus(
+            int unarchiveId,
+            @UnarchivalStatus int status,
+            long requiredStorageBytes,
+            @Nullable PendingIntent userActionIntent,
+            @NonNull UserHandle userHandle) {
+        verifyReportUnarchiveStatusInput(
+                status, requiredStorageBytes, userActionIntent, userHandle);
+
+        int userId = userHandle.getIdentifier();
+        int binderUid = Binder.getCallingUid();
+
+        synchronized (mSessions) {
+            PackageInstallerSession session = mSessions.get(unarchiveId);
+            if (session == null || session.userId != userId
+                    || session.params.appPackageName == null) {
+                throw new ParcelableException(new PackageManager.NameNotFoundException(
+                        TextUtils.formatSimple(
+                                "No valid session with unarchival ID %s found for user %s.",
+                                unarchiveId, userId)));
+            }
+
+            if (!isCallingUidOwner(session)) {
+                throw new SecurityException(TextUtils.formatSimple(
+                        "The caller UID %s does not have access to the session with unarchiveId "
+                                + "%d.",
+                        binderUid, unarchiveId));
+            }
+
+            IntentSender unarchiveIntentSender = session.params.unarchiveIntentSender;
+            if (unarchiveIntentSender == null) {
+                throw new IllegalStateException(
+                        TextUtils.formatSimple(
+                                "Unarchival status for ID %s has already been set or a "
+                                        + "session has been created for it already by the "
+                                        + "caller.",
+                                unarchiveId));
+            }
+
+            // Execute expensive calls outside the sync block.
+            mPm.mHandler.post(
+                    () -> notifyUnarchivalListener(status, session.params.appPackageName,
+                            unarchiveIntentSender));
+            session.params.unarchiveIntentSender = null;
+            if (status != UNARCHIVAL_OK) {
+                Binder.withCleanCallingIdentity(session::abandon);
+            }
+        }
+    }
+
+    private static void verifyReportUnarchiveStatusInput(int status, long requiredStorageBytes,
+            @Nullable PendingIntent userActionIntent,
+            @NonNull UserHandle userHandle) {
+        Objects.requireNonNull(userHandle);
+        if (status == UNARCHIVAL_ERROR_USER_ACTION_NEEDED) {
+            Objects.requireNonNull(userActionIntent);
+        }
+        if (status == UNARCHIVAL_ERROR_INSUFFICIENT_STORAGE && requiredStorageBytes <= 0) {
+            throw new IllegalStateException(
+                    "Insufficient storage error set, but requiredStorageBytes unspecified.");
+        }
+        if (status != UNARCHIVAL_ERROR_INSUFFICIENT_STORAGE && requiredStorageBytes > 0) {
+            throw new IllegalStateException(
+                    TextUtils.formatSimple("requiredStorageBytes set, but error is %s.", status)
+            );
+        }
+        if (!List.of(
+                UNARCHIVAL_OK,
+                UNARCHIVAL_ERROR_USER_ACTION_NEEDED,
+                UNARCHIVAL_ERROR_INSUFFICIENT_STORAGE,
+                UNARCHIVAL_ERROR_NO_CONNECTIVITY,
+                UNARCHIVAL_GENERIC_ERROR).contains(status)) {
+            throw new IllegalStateException("Invalid status code passed " + status);
+        }
+    }
+
+    private void notifyUnarchivalListener(int status, String packageName,
+            IntentSender unarchiveIntentSender) {
+        final Intent fillIn = new Intent();
+        fillIn.putExtra(PackageInstaller.EXTRA_PACKAGE_NAME, packageName);
+        fillIn.putExtra(PackageInstaller.EXTRA_UNARCHIVE_STATUS, status);
+        // TODO(b/307299702) Attach failure dialog with EXTRA_INTENT and requiredStorageBytes here.
+        final BroadcastOptions options = BroadcastOptions.makeBasic();
+        options.setPendingIntentBackgroundActivityStartMode(
+                MODE_BACKGROUND_ACTIVITY_START_DENIED);
+        try {
+            unarchiveIntentSender.sendIntent(mContext, 0, fillIn, /* onFinished= */ null,
+                    /* handler= */ null, /* requiredPermission= */ null,
+                    options.toBundle());
+        } catch (SendIntentException e) {
+            Slog.e(TAG, TextUtils.formatSimple("Failed to send unarchive intent"), e);
+        }
+    }
+
     private static int getSessionCount(SparseArray<PackageInstallerSession> sessions,
             int installerUid) {
         int count = 0;
diff --git a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
index 6f45d2b..f992bd8 100644
--- a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
+++ b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
@@ -4689,10 +4689,12 @@
 
         final int translatedUserId =
                 translateUserId(userId, UserHandle.USER_SYSTEM, "runArchive");
+        final LocalIntentReceiver receiver = new LocalIntentReceiver();
 
         try {
             mInterface.getPackageInstaller().requestUnarchive(packageName,
-                    /* callerPackageName= */ "", new UserHandle(translatedUserId));
+                    /* callerPackageName= */ "", receiver.getIntentSender(),
+                    new UserHandle(translatedUserId));
         } catch (Exception e) {
             pw.println("Failure [" + e.getMessage() + "]");
             return 1;
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverTest.java
index a9f5b14..18a2acc 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverTest.java
@@ -37,6 +37,7 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.Manifest;
 import android.app.ActivityManager;
 import android.app.AppOpsManager;
 import android.content.ComponentName;
@@ -171,6 +172,12 @@
 
         when(mContext.getSystemService(ActivityManager.class)).thenReturn(mActivityManager);
         when(mActivityManager.getLauncherLargeIconDensity()).thenReturn(100);
+        when(mContext.checkCallingOrSelfPermission(
+                eq(Manifest.permission.REQUEST_INSTALL_PACKAGES))).thenReturn(
+                PackageManager.PERMISSION_DENIED);
+        when(mContext.checkCallingOrSelfPermission(
+                eq(Manifest.permission.REQUEST_DELETE_PACKAGES))).thenReturn(
+                PackageManager.PERMISSION_DENIED);
 
         when(mAppOpsManager.checkOp(
                 eq(AppOpsManager.OP_AUTO_REVOKE_PERMISSIONS_IF_UNUSED),
@@ -386,7 +393,7 @@
         Exception e = assertThrows(
                 SecurityException.class,
                 () -> mArchiveManager.requestUnarchive(PACKAGE, "different",
-                        UserHandle.CURRENT));
+                        mIntentSender, UserHandle.CURRENT));
         assertThat(e).hasMessageThat().isEqualTo(
                 String.format(
                         "The UID %s of callerPackageName set by the caller doesn't match the "
@@ -404,7 +411,7 @@
         Exception e = assertThrows(
                 ParcelableException.class,
                 () -> mArchiveManager.requestUnarchive(PACKAGE, CALLER_PACKAGE,
-                        UserHandle.CURRENT));
+                        mIntentSender, UserHandle.CURRENT));
         assertThat(e.getCause()).isInstanceOf(PackageManager.NameNotFoundException.class);
         assertThat(e.getCause()).hasMessageThat().isEqualTo(
                 String.format("Package %s not found.", PACKAGE));
@@ -416,7 +423,7 @@
         Exception e = assertThrows(
                 ParcelableException.class,
                 () -> mArchiveManager.requestUnarchive(PACKAGE, CALLER_PACKAGE,
-                        UserHandle.CURRENT));
+                        mIntentSender, UserHandle.CURRENT));
         assertThat(e.getCause()).isInstanceOf(PackageManager.NameNotFoundException.class);
         assertThat(e.getCause()).hasMessageThat().isEqualTo(
                 String.format("Package %s is not currently archived.", PACKAGE));
@@ -428,7 +435,7 @@
         Exception e = assertThrows(
                 ParcelableException.class,
                 () -> mArchiveManager.requestUnarchive(PACKAGE, CALLER_PACKAGE,
-                        UserHandle.CURRENT));
+                        mIntentSender, UserHandle.CURRENT));
         assertThat(e.getCause()).isInstanceOf(PackageManager.NameNotFoundException.class);
         assertThat(e.getCause()).hasMessageThat().isEqualTo(
                 String.format("Package %s is not currently archived.", PACKAGE));
@@ -452,7 +459,7 @@
         Exception e = assertThrows(
                 ParcelableException.class,
                 () -> mArchiveManager.requestUnarchive(PACKAGE, CALLER_PACKAGE,
-                        UserHandle.CURRENT));
+                        mIntentSender, UserHandle.CURRENT));
         assertThat(e.getCause()).isInstanceOf(PackageManager.NameNotFoundException.class);
         assertThat(e.getCause()).hasMessageThat().isEqualTo(
                 String.format("No installer found to unarchive app %s.", PACKAGE));
@@ -462,7 +469,8 @@
     public void unarchiveApp_success() {
         mUserState.setArchiveState(createArchiveState()).setInstalled(false);
 
-        mArchiveManager.requestUnarchive(PACKAGE, CALLER_PACKAGE, UserHandle.CURRENT);
+        mArchiveManager.requestUnarchive(PACKAGE, CALLER_PACKAGE, mIntentSender,
+                UserHandle.CURRENT);
         rule.mocks().getHandler().flush();
 
         ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);