Merge "Add basic implementation of the requestUnarchive() API." into main
diff --git a/apex/jobscheduler/framework/java/android/os/PowerExemptionManager.java b/apex/jobscheduler/framework/java/android/os/PowerExemptionManager.java
index 17076bc..66c1efc 100644
--- a/apex/jobscheduler/framework/java/android/os/PowerExemptionManager.java
+++ b/apex/jobscheduler/framework/java/android/os/PowerExemptionManager.java
@@ -419,6 +419,14 @@
      */
     public static final int REASON_SYSTEM_EXEMPT_APP_OP = 327;
 
+    /**
+     * Granted by {@link com.android.server.pm.PackageArchiverService} to the installer responsible
+     * for unarchiving an app.
+     *
+     * @hide
+     */
+    public static final int REASON_PACKAGE_UNARCHIVE = 328;
+
     /** @hide The app requests out-out. */
     public static final int REASON_OPT_OUT_REQUESTED = 1000;
 
@@ -502,6 +510,7 @@
             REASON_ACTIVE_DEVICE_ADMIN,
             REASON_MEDIA_NOTIFICATION_TRANSFER,
             REASON_PACKAGE_INSTALLER,
+            REASON_PACKAGE_UNARCHIVE,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface ReasonCode {}
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 35a736d..0c61981 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -3547,6 +3547,7 @@
     field public static final String ACTION_SHOW_SUSPENDED_APP_DETAILS = "android.intent.action.SHOW_SUSPENDED_APP_DETAILS";
     field @Deprecated public static final String ACTION_SIM_STATE_CHANGED = "android.intent.action.SIM_STATE_CHANGED";
     field public static final String ACTION_SPLIT_CONFIGURATION_CHANGED = "android.intent.action.SPLIT_CONFIGURATION_CHANGED";
+    field public static final String ACTION_UNARCHIVE_PACKAGE = "android.intent.action.UNARCHIVE_PACKAGE";
     field public static final String ACTION_UPGRADE_SETUP = "android.intent.action.UPGRADE_SETUP";
     field public static final String ACTION_USER_ADDED = "android.intent.action.USER_ADDED";
     field public static final String ACTION_USER_REMOVED = "android.intent.action.USER_REMOVED";
@@ -3796,6 +3797,9 @@
 
   public class PackageArchiver {
     method @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 @RequiresPermission(anyOf={android.Manifest.permission.INSTALL_PACKAGES, android.Manifest.permission.REQUEST_INSTALL_PACKAGES}) public void requestUnarchive(@NonNull String) throws android.content.pm.PackageManager.NameNotFoundException;
+    field public static final String EXTRA_UNARCHIVE_ALL_USERS = "android.content.pm.extra.UNARCHIVE_ALL_USERS";
+    field public static final String EXTRA_UNARCHIVE_PACKAGE_NAME = "android.content.pm.extra.UNARCHIVE_PACKAGE_NAME";
   }
 
   public class PackageInfo implements android.os.Parcelable {
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java
index 31f6418..fe7d1e6 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -5270,6 +5270,16 @@
     public static final String ACTION_SHOW_FOREGROUND_SERVICE_MANAGER =
             "android.intent.action.SHOW_FOREGROUND_SERVICE_MANAGER";
 
+    /**
+     * Broadcast Action: Sent to the responsible installer of an archived package when unarchival
+     * is requested.
+     *
+     * @see android.content.pm.PackageArchiver
+     * @hide
+     */
+    @SystemApi
+    public static final String ACTION_UNARCHIVE_PACKAGE = "android.intent.action.UNARCHIVE_PACKAGE";
+
     // ---------------------------------------------------------------------
     // ---------------------------------------------------------------------
     // Standard intent categories (see addCategory()).
diff --git a/core/java/android/content/pm/IPackageArchiverService.aidl b/core/java/android/content/pm/IPackageArchiverService.aidl
index fc471c4..dc6491d 100644
--- a/core/java/android/content/pm/IPackageArchiverService.aidl
+++ b/core/java/android/content/pm/IPackageArchiverService.aidl
@@ -23,4 +23,7 @@
 
     @JavaPassthrough(annotation="@android.annotation.RequiresPermission(anyOf={android.Manifest.permission.DELETE_PACKAGES,android.Manifest.permission.REQUEST_DELETE_PACKAGES})")
     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);
 }
\ No newline at end of file
diff --git a/core/java/android/content/pm/PackageArchiver.java b/core/java/android/content/pm/PackageArchiver.java
index d739d50..b065231 100644
--- a/core/java/android/content/pm/PackageArchiver.java
+++ b/core/java/android/content/pm/PackageArchiver.java
@@ -42,6 +42,26 @@
 @SystemApi
 public class PackageArchiver {
 
+    /**
+     * Extra field for the package name of a package that is requested to be unarchived. Sent as
+     * part of the {@link android.content.Intent#ACTION_UNARCHIVE_PACKAGE} intent.
+     *
+     * @hide
+     */
+    @SystemApi
+    public static final String EXTRA_UNARCHIVE_PACKAGE_NAME =
+            "android.content.pm.extra.UNARCHIVE_PACKAGE_NAME";
+
+    /**
+     * If true, the requestor of the unarchival has specified that the app should be unarchived
+     * for {@link android.os.UserHandle#ALL}.
+     *
+     * @hide
+     */
+    @SystemApi
+    public static final String EXTRA_UNARCHIVE_ALL_USERS =
+            "android.content.pm.extra.UNARCHIVE_ALL_USERS";
+
     private final Context mContext;
     private final IPackageArchiverService mService;
 
@@ -58,7 +78,7 @@
      *
      * @param statusReceiver Callback used to notify when the operation is completed.
      * @throws NameNotFoundException If {@code packageName} isn't found or not available to the
-     *                               caller.
+     *                               caller or isn't archived.
      * @hide
      */
     @RequiresPermission(anyOf = {
@@ -76,4 +96,34 @@
             throw e.rethrowFromSystemServer();
         }
     }
+
+    /**
+     * Requests to unarchive a currently archived package.
+     *
+     * <p> Sends a request to unarchive an app to the responsible installer. The installer is
+     * determined by {@link InstallSourceInfo#getUpdateOwnerPackageName()}, or
+     * {@link InstallSourceInfo#getInstallingPackageName()} if the former value is null.
+     *
+     * <p> The installation will happen asynchronously and can be observed through
+     * {@link android.content.Intent#ACTION_PACKAGE_ADDED}.
+     *
+     * @throws 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.
+     * @hide
+     */
+    @RequiresPermission(anyOf = {
+            Manifest.permission.INSTALL_PACKAGES,
+            Manifest.permission.REQUEST_INSTALL_PACKAGES})
+    @SystemApi
+    public void requestUnarchive(@NonNull String packageName)
+            throws NameNotFoundException {
+        try {
+            mService.requestUnarchive(packageName, mContext.getPackageName(), mContext.getUser());
+        } catch (ParcelableException e) {
+            e.maybeRethrow(NameNotFoundException.class);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/pm/PackageArchiverService.java b/services/core/java/com/android/server/pm/PackageArchiverService.java
index 9c31dc9..1b25d6d 100644
--- a/services/core/java/com/android/server/pm/PackageArchiverService.java
+++ b/services/core/java/com/android/server/pm/PackageArchiverService.java
@@ -17,17 +17,26 @@
 package com.android.server.pm;
 
 import static android.content.pm.PackageManager.DELETE_KEEP_DATA;
+import static android.os.PowerExemptionManager.REASON_PACKAGE_UNARCHIVE;
+import static android.os.PowerExemptionManager.TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_ALLOWED;
 
+import android.Manifest;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.app.AppOpsManager;
+import android.app.BroadcastOptions;
 import android.content.Context;
+import android.content.Intent;
 import android.content.IntentSender;
 import android.content.pm.IPackageArchiverService;
 import android.content.pm.LauncherActivityInfo;
 import android.content.pm.LauncherApps;
+import android.content.pm.PackageArchiver;
 import android.content.pm.PackageManager;
 import android.content.pm.VersionedPackage;
 import android.os.Binder;
+import android.os.Bundle;
 import android.os.ParcelableException;
 import android.os.UserHandle;
 import android.text.TextUtils;
@@ -36,6 +45,7 @@
 import com.android.server.pm.pkg.ArchiveState;
 import com.android.server.pm.pkg.ArchiveState.ArchiveActivityInfo;
 import com.android.server.pm.pkg.PackageStateInternal;
+import com.android.server.pm.pkg.PackageUserStateInternal;
 
 import java.nio.file.Path;
 import java.util.ArrayList;
@@ -53,6 +63,13 @@
 
     private static final String TAG = "PackageArchiver";
 
+    /**
+     * The maximum time granted for an app store to start a foreground service when unarchival
+     * is requested.
+     */
+    // TODO(b/297358628) Make this configurable through a flag.
+    private static final int DEFAULT_UNARCHIVE_FOREGROUND_TIMEOUT_MS = 120 * 1000;
+
     private final Context mContext;
     private final PackageManagerService mPm;
 
@@ -83,7 +100,12 @@
                 "archiveApp");
         verifyCaller(providedUid, binderUid);
         PackageStateInternal ps = getPackageState(packageName, snapshot, binderUid, userId);
-        verifyInstaller(packageName, ps);
+        if (getResponsibleInstallerPackage(ps) == null) {
+            throw new ParcelableException(
+                    new PackageManager.NameNotFoundException(
+                            TextUtils.formatSimple("No installer found to archive app %s.",
+                                    packageName)));
+        }
 
         // TODO(b/291569242) Verify that this list is not empty and return failure with
         //  intentsender
@@ -100,14 +122,99 @@
                 callerPackageName, DELETE_KEEP_DATA, intentSender, userId);
     }
 
-    private static void verifyInstaller(String packageName, PackageStateInternal ps) {
-        if (ps.getInstallSource().mUpdateOwnerPackageName == null
-                && ps.getInstallSource().mInstallerPackageName == null) {
+    @Override
+    public void requestUnarchive(
+            @NonNull String packageName,
+            @NonNull String callerPackageName,
+            @NonNull UserHandle userHandle) {
+        Objects.requireNonNull(packageName);
+        Objects.requireNonNull(callerPackageName);
+        Objects.requireNonNull(userHandle);
+
+        Computer snapshot = mPm.snapshotComputer();
+        int userId = userHandle.getIdentifier();
+        int binderUid = Binder.getCallingUid();
+        int providedUid = snapshot.getPackageUid(callerPackageName, 0, userId);
+        snapshot.enforceCrossUserPermission(binderUid, userId, true, true,
+                "unarchiveApp");
+        verifyCaller(providedUid, binderUid);
+        PackageStateInternal ps = getPackageState(packageName, snapshot, binderUid, userId);
+        verifyArchived(ps, userId);
+        String installerPackage = getResponsibleInstallerPackage(ps);
+        if (installerPackage == null) {
             throw new ParcelableException(
                     new PackageManager.NameNotFoundException(
-                            TextUtils.formatSimple("No installer found to archive app %s.",
+                            TextUtils.formatSimple("No installer found to unarchive app %s.",
                                     packageName)));
         }
+
+        mPm.mHandler.post(() -> unarchiveInternal(packageName, userHandle, installerPackage));
+    }
+
+    private void verifyArchived(PackageStateInternal ps, int userId) {
+        PackageUserStateInternal userState = ps.getUserStateOrDefault(userId);
+        // TODO(b/288142708) Check for isInstalled false here too.
+        if (userState.getArchiveState() == null) {
+            throw new ParcelableException(
+                    new PackageManager.NameNotFoundException(
+                            TextUtils.formatSimple("Package %s is not currently archived.",
+                                    ps.getPackageName())));
+        }
+    }
+
+    @RequiresPermission(
+            allOf = {
+                    Manifest.permission.INTERACT_ACROSS_USERS,
+                    android.Manifest.permission.CHANGE_DEVICE_IDLE_TEMP_WHITELIST,
+                    android.Manifest.permission.START_ACTIVITIES_FROM_BACKGROUND,
+                    android.Manifest.permission.START_FOREGROUND_SERVICES_FROM_BACKGROUND},
+            conditional = true)
+    private void unarchiveInternal(String packageName, UserHandle userHandle,
+            String installerPackage) {
+        int userId = userHandle.getIdentifier();
+        Intent unarchiveIntent = new Intent(Intent.ACTION_UNARCHIVE_PACKAGE);
+        unarchiveIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
+        unarchiveIntent.putExtra(PackageArchiver.EXTRA_UNARCHIVE_PACKAGE_NAME, packageName);
+        unarchiveIntent.putExtra(PackageArchiver.EXTRA_UNARCHIVE_ALL_USERS,
+                userId == UserHandle.USER_ALL);
+        unarchiveIntent.setPackage(installerPackage);
+
+        // If the unarchival is requested for all users, the current user is used for unarchival.
+        UserHandle userForUnarchival = userId == UserHandle.USER_ALL
+                ? UserHandle.of(mPm.mUserManager.getCurrentUserId())
+                : userHandle;
+        mContext.sendOrderedBroadcastAsUser(
+                unarchiveIntent,
+                userForUnarchival,
+                /* receiverPermission = */ null,
+                AppOpsManager.OP_NONE,
+                createUnarchiveOptions(),
+                /* resultReceiver= */ null,
+                /* scheduler= */ null,
+                /* initialCode= */ 0,
+                /* initialData= */ null,
+                /* initialExtras= */ null);
+    }
+
+    @RequiresPermission(anyOf = {android.Manifest.permission.CHANGE_DEVICE_IDLE_TEMP_WHITELIST,
+            android.Manifest.permission.START_ACTIVITIES_FROM_BACKGROUND,
+            android.Manifest.permission.START_FOREGROUND_SERVICES_FROM_BACKGROUND})
+    private Bundle createUnarchiveOptions() {
+        BroadcastOptions options = BroadcastOptions.makeBasic();
+        options.setTemporaryAppAllowlist(getUnarchiveForegroundTimeout(),
+                TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_ALLOWED,
+                REASON_PACKAGE_UNARCHIVE, "");
+        return options.toBundle();
+    }
+
+    private static int getUnarchiveForegroundTimeout() {
+        return DEFAULT_UNARCHIVE_FOREGROUND_TIMEOUT_MS;
+    }
+
+    private String getResponsibleInstallerPackage(PackageStateInternal ps) {
+        return ps.getInstallSource().mUpdateOwnerPackageName == null
+                ? ps.getInstallSource().mInstallerPackageName
+                : ps.getInstallSource().mUpdateOwnerPackageName;
     }
 
     @NonNull
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverServiceTest.java
index c7e1bda..473cece 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverServiceTest.java
@@ -16,27 +16,34 @@
 
 package com.android.server.pm;
 
+import static android.content.Intent.FLAG_RECEIVER_FOREGROUND;
 import static android.content.pm.PackageManager.DELETE_KEEP_DATA;
 
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.app.AppOpsManager;
 import android.content.Context;
+import android.content.Intent;
 import android.content.IntentSender;
 import android.content.pm.LauncherActivityInfo;
 import android.content.pm.LauncherApps;
+import android.content.pm.PackageArchiver;
 import android.content.pm.PackageManager;
 import android.content.pm.VersionedPackage;
 import android.os.Binder;
 import android.os.Build;
+import android.os.Bundle;
 import android.os.ParcelableException;
 import android.os.UserHandle;
 import android.platform.test.annotations.Presubmit;
@@ -46,11 +53,13 @@
 
 import com.android.server.pm.pkg.ArchiveState;
 import com.android.server.pm.pkg.PackageStateInternal;
+import com.android.server.pm.pkg.PackageUserStateImpl;
 
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
@@ -64,7 +73,9 @@
 public class PackageArchiverServiceTest {
 
     private static final String PACKAGE = "com.example";
-    private static final String CALLER_PACKAGE = "com.vending";
+    private static final String CALLER_PACKAGE = "com.caller";
+    private static final String INSTALLER_PACKAGE = "com.installer";
+
 
     @Rule
     public final MockSystemRule mMockSystem = new MockSystemRule();
@@ -84,11 +95,11 @@
 
     private final InstallSource mInstallSource =
             InstallSource.create(
-                    CALLER_PACKAGE,
-                    CALLER_PACKAGE,
-                    CALLER_PACKAGE,
+                    INSTALLER_PACKAGE,
+                    INSTALLER_PACKAGE,
+                    INSTALLER_PACKAGE,
                     Binder.getCallingUid(),
-                    CALLER_PACKAGE,
+                    INSTALLER_PACKAGE,
                     /* installerAttributionTag= */ null,
                     /* packageSource= */ 0);
 
@@ -96,6 +107,8 @@
 
     private final int mUserId = UserHandle.CURRENT.getIdentifier();
 
+    private PackageUserStateImpl mUserState;
+
     private PackageSetting mPackageSetting;
 
     private PackageArchiverService mArchiveService;
@@ -116,11 +129,16 @@
 
         when(mComputer.getPackageStateFiltered(eq(PACKAGE), anyInt(), anyInt())).thenReturn(
                 mPackageState);
+        when(mComputer.getPackageStateFiltered(eq(INSTALLER_PACKAGE), anyInt(),
+                anyInt())).thenReturn(mock(PackageStateInternal.class));
         when(mPackageState.getPackageName()).thenReturn(PACKAGE);
         when(mPackageState.getInstallSource()).thenReturn(mInstallSource);
         mPackageSetting = createBasicPackageSetting();
         when(mMockSystem.mocks().getSettings().getPackageLPr(eq(PACKAGE))).thenReturn(
                 mPackageSetting);
+        mUserState = new PackageUserStateImpl().setInstalled(true);
+        mPackageSetting.setUserState(mUserId, mUserState);
+        when(mPackageState.getUserStateOrDefault(eq(mUserId))).thenReturn(mUserState);
         when(mContext.getSystemService(LauncherApps.class)).thenReturn(mLauncherApps);
         when(mLauncherApps.getActivityList(eq(PACKAGE), eq(UserHandle.CURRENT))).thenReturn(
                 mLauncherActivityInfos);
@@ -135,9 +153,7 @@
         Exception e = assertThrows(
                 SecurityException.class,
                 () -> mArchiveService.requestArchive(PACKAGE, "different", mIntentSender,
-                        UserHandle.CURRENT
-                )
-        );
+                        UserHandle.CURRENT));
         assertThat(e).hasMessageThat().isEqualTo(
                 String.format(
                         "The UID %s of callerPackageName set by the caller doesn't match the "
@@ -154,9 +170,7 @@
         Exception e = assertThrows(
                 ParcelableException.class,
                 () -> mArchiveService.requestArchive(PACKAGE, CALLER_PACKAGE, mIntentSender,
-                        UserHandle.CURRENT
-                )
-        );
+                        UserHandle.CURRENT));
         assertThat(e.getCause()).isInstanceOf(PackageManager.NameNotFoundException.class);
         assertThat(e.getCause()).hasMessageThat().isEqualTo(
                 String.format("Package %s not found.", PACKAGE));
@@ -169,9 +183,7 @@
         Exception e = assertThrows(
                 ParcelableException.class,
                 () -> mArchiveService.requestArchive(PACKAGE, CALLER_PACKAGE, mIntentSender,
-                        UserHandle.CURRENT
-                )
-        );
+                        UserHandle.CURRENT));
         assertThat(e.getCause()).isInstanceOf(PackageManager.NameNotFoundException.class);
         assertThat(e.getCause()).hasMessageThat().isEqualTo(
                 String.format("Package %s not found.", PACKAGE));
@@ -193,9 +205,7 @@
         Exception e = assertThrows(
                 ParcelableException.class,
                 () -> mArchiveService.requestArchive(PACKAGE, CALLER_PACKAGE, mIntentSender,
-                        UserHandle.CURRENT
-                )
-        );
+                        UserHandle.CURRENT));
         assertThat(e.getCause()).isInstanceOf(PackageManager.NameNotFoundException.class);
         assertThat(e.getCause()).hasMessageThat().isEqualTo(
                 String.format("No installer found to archive app %s.", PACKAGE));
@@ -203,15 +213,6 @@
 
     @Test
     public void archiveApp_success() {
-        List<ArchiveState.ArchiveActivityInfo> activityInfos = new ArrayList<>();
-        for (LauncherActivityInfo mainActivity : createLauncherActivities()) {
-            // TODO(b/278553670) Extract and store launcher icons
-            ArchiveState.ArchiveActivityInfo activityInfo = new ArchiveState.ArchiveActivityInfo(
-                    mainActivity.getLabel().toString(),
-                    Path.of("/TODO"), null);
-            activityInfos.add(activityInfo);
-        }
-
         mArchiveService.requestArchive(PACKAGE, CALLER_PACKAGE, mIntentSender, UserHandle.CURRENT);
 
         verify(mInstallerService).uninstall(
@@ -220,7 +221,112 @@
                 eq(UserHandle.CURRENT.getIdentifier()));
         assertThat(mPackageSetting.readUserState(
                 UserHandle.CURRENT.getIdentifier()).getArchiveState()).isEqualTo(
-                new ArchiveState(activityInfos, CALLER_PACKAGE));
+                createArchiveState());
+    }
+
+    @Test
+    public void unarchiveApp_callerPackageNameIncorrect() {
+        mUserState.setArchiveState(createArchiveState()).setInstalled(false);
+
+        Exception e = assertThrows(
+                SecurityException.class,
+                () -> mArchiveService.requestUnarchive(PACKAGE, "different",
+                        UserHandle.CURRENT));
+        assertThat(e).hasMessageThat().isEqualTo(
+                String.format(
+                        "The UID %s of callerPackageName set by the caller doesn't match the "
+                                + "caller's actual UID %s.",
+                        0,
+                        Binder.getCallingUid()));
+    }
+
+    @Test
+    public void unarchiveApp_packageNotInstalled() {
+        mUserState.setArchiveState(createArchiveState()).setInstalled(false);
+        when(mComputer.getPackageStateFiltered(eq(PACKAGE), anyInt(), anyInt())).thenReturn(
+                null);
+
+        Exception e = assertThrows(
+                ParcelableException.class,
+                () -> mArchiveService.requestUnarchive(PACKAGE, CALLER_PACKAGE,
+                        UserHandle.CURRENT));
+        assertThat(e.getCause()).isInstanceOf(PackageManager.NameNotFoundException.class);
+        assertThat(e.getCause()).hasMessageThat().isEqualTo(
+                String.format("Package %s not found.", PACKAGE));
+    }
+
+    @Test
+    public void unarchiveApp_notArchived() {
+        Exception e = assertThrows(
+                ParcelableException.class,
+                () -> mArchiveService.requestUnarchive(PACKAGE, CALLER_PACKAGE,
+                        UserHandle.CURRENT));
+        assertThat(e.getCause()).isInstanceOf(PackageManager.NameNotFoundException.class);
+        assertThat(e.getCause()).hasMessageThat().isEqualTo(
+                String.format("Package %s is not currently archived.", PACKAGE));
+    }
+
+    @Test
+    public void unarchiveApp_noInstallerFound() {
+        mUserState.setArchiveState(createArchiveState());
+        InstallSource otherInstallSource =
+                InstallSource.create(
+                        CALLER_PACKAGE,
+                        CALLER_PACKAGE,
+                        /* installerPackageName= */ null,
+                        Binder.getCallingUid(),
+                        /* updateOwnerPackageName= */ null,
+                        /* installerAttributionTag= */ null,
+                        /* packageSource= */ 0);
+        when(mPackageState.getInstallSource()).thenReturn(otherInstallSource);
+
+        Exception e = assertThrows(
+                ParcelableException.class,
+                () -> mArchiveService.requestUnarchive(PACKAGE, CALLER_PACKAGE,
+                        UserHandle.CURRENT));
+        assertThat(e.getCause()).isInstanceOf(PackageManager.NameNotFoundException.class);
+        assertThat(e.getCause()).hasMessageThat().isEqualTo(
+                String.format("No installer found to unarchive app %s.", PACKAGE));
+    }
+
+    @Test
+    public void unarchiveApp_success() {
+        mUserState.setArchiveState(createArchiveState()).setInstalled(false);
+
+        mArchiveService.requestUnarchive(PACKAGE, CALLER_PACKAGE, UserHandle.CURRENT);
+        mMockSystem.mocks().getHandler().flush();
+
+        ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
+        verify(mContext).sendOrderedBroadcastAsUser(
+                intentCaptor.capture(),
+                eq(UserHandle.CURRENT),
+                /* receiverPermission = */ isNull(),
+                eq(AppOpsManager.OP_NONE),
+                any(Bundle.class),
+                /* resultReceiver= */ isNull(),
+                /* scheduler= */ isNull(),
+                /* initialCode= */ eq(0),
+                /* initialData= */ isNull(),
+                /* initialExtras= */ isNull());
+        Intent intent = intentCaptor.getValue();
+        assertThat(intent.getFlags() & FLAG_RECEIVER_FOREGROUND).isNotEqualTo(0);
+        assertThat(intent.getStringExtra(PackageArchiver.EXTRA_UNARCHIVE_PACKAGE_NAME)).isEqualTo(
+                PACKAGE);
+        assertThat(
+                intent.getBooleanExtra(PackageArchiver.EXTRA_UNARCHIVE_ALL_USERS, true)).isFalse();
+        assertThat(intent.getPackage()).isEqualTo(INSTALLER_PACKAGE);
+    }
+
+    private static ArchiveState createArchiveState() {
+        List<ArchiveState.ArchiveActivityInfo> activityInfos = new ArrayList<>();
+        for (LauncherActivityInfo mainActivity : createLauncherActivities()) {
+            // TODO(b/278553670) Extract and store launcher icons
+            ArchiveState.ArchiveActivityInfo activityInfo = new ArchiveState.ArchiveActivityInfo(
+                    mainActivity.getLabel().toString(),
+                    Path.of("/TODO"), null);
+            activityInfos.add(activityInfo);
+        }
+        return new ArchiveState(activityInfos, INSTALLER_PACKAGE);
     }
 
     private static List<LauncherActivityInfo> createLauncherActivities() {