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() {