Merge "Adding ArchiveManager skeleton with some basic validations"
diff --git a/services/core/java/com/android/server/pm/ArchiveManager.java b/services/core/java/com/android/server/pm/ArchiveManager.java
new file mode 100644
index 0000000..5082be6
--- /dev/null
+++ b/services/core/java/com/android/server/pm/ArchiveManager.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.pm;
+
+import android.annotation.NonNull;
+import android.content.IntentSender;
+import android.content.pm.PackageManager;
+import android.os.Binder;
+import android.text.TextUtils;
+
+import com.android.server.pm.pkg.PackageStateInternal;
+
+import java.util.Objects;
+
+/**
+ * Responsible archiving apps and returning information about archived apps.
+ *
+ * <p> An archived app is in a state where the app is not fully on the device. APKs are removed
+ * while the data directory is kept. Archived apps are included in the list of launcher apps where
+ * tapping them re-installs the full app.
+ */
+final class ArchiveManager {
+
+    private final PackageManagerService mPm;
+
+    ArchiveManager(PackageManagerService mPm) {
+        this.mPm = mPm;
+    }
+
+    void archiveApp(
+            @NonNull String packageName,
+            @NonNull String callerPackageName,
+            int userId,
+            @NonNull IntentSender intentSender) throws PackageManager.NameNotFoundException {
+        Objects.requireNonNull(packageName);
+        Objects.requireNonNull(callerPackageName);
+        Objects.requireNonNull(intentSender);
+
+        Computer snapshot = mPm.snapshotComputer();
+        int callingUid = Binder.getCallingUid();
+        String callingPackageName = snapshot.getNameForUid(callingUid);
+        snapshot.enforceCrossUserPermission(callingUid, userId, true, true, "archiveApp");
+        verifyCaller(callerPackageName, callingPackageName);
+
+        PackageStateInternal ps = snapshot.getPackageStateInternal(packageName);
+        if (ps == null) {
+            throw new PackageManager.NameNotFoundException(
+                    TextUtils.formatSimple("Package %s not found.", packageName));
+        }
+
+        verifyInstallOwnership(packageName, callingPackageName, ps);
+
+        // TODO(b/278553670) Complete implementations
+        throw new UnsupportedOperationException("Method not implemented.");
+    }
+
+    private static void verifyCaller(String callerPackageName, String callingPackageName) {
+        if (!TextUtils.equals(callingPackageName, callerPackageName)) {
+            throw new SecurityException(
+                    TextUtils.formatSimple(
+                            "The callerPackageName %s set by the caller doesn't match the "
+                                    + "caller's own package name %s.",
+                            callerPackageName,
+                            callingPackageName));
+        }
+    }
+
+    private static void verifyInstallOwnership(String packageName, String callingPackageName,
+            PackageStateInternal ps) {
+        if (!TextUtils.equals(ps.getInstallSource().mInstallerPackageName,
+                callingPackageName)) {
+            throw new SecurityException(
+                    TextUtils.formatSimple("Caller is not the installer of record for %s.",
+                            packageName));
+        }
+        String updateOwnerPackageName = ps.getInstallSource().mUpdateOwnerPackageName;
+        if (updateOwnerPackageName != null
+                && !TextUtils.equals(updateOwnerPackageName, callingPackageName)) {
+            throw new SecurityException(
+                    TextUtils.formatSimple("Caller is not the update owner for %s.", packageName));
+        }
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/pm/ArchiveManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/ArchiveManagerTest.java
new file mode 100644
index 0000000..73a6bb7
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/pm/ArchiveManagerTest.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.pm;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.when;
+
+import android.content.IntentSender;
+import android.content.pm.PackageManager;
+import android.os.Binder;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.pm.pkg.PackageStateInternal;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class ArchiveManagerTest {
+
+    private static final String PACKAGE = "com.example";
+    private static final String CALLER_PACKAGE = "com.vending";
+    private static final int USER_ID = 1;
+
+    @Mock private IntentSender mIntentSender;
+    @Mock private PackageManagerService mPm;
+    @Mock private Computer mComputer;
+    @Mock private PackageStateInternal mPackageState;
+    private InstallSource mInstallSource =
+            InstallSource.create(
+                    CALLER_PACKAGE,
+                    CALLER_PACKAGE,
+                    CALLER_PACKAGE,
+                    Binder.getCallingUid(),
+                    CALLER_PACKAGE,
+                    /* installerAttributionTag= */ null,
+                    /* packageSource= */ 0);
+
+    private ArchiveManager mArchiveManager;
+    private int mCallingUid;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        mArchiveManager = new ArchiveManager(mPm);
+        mCallingUid = Binder.getCallingUid();
+        when(mPm.snapshotComputer()).thenReturn(mComputer);
+        when(mComputer.getNameForUid(eq(mCallingUid))).thenReturn(CALLER_PACKAGE);
+        when(mComputer.getPackageStateInternal(eq(PACKAGE))).thenReturn(mPackageState);
+        when(mPackageState.getInstallSource()).thenReturn(mInstallSource);
+    }
+
+    @Test
+    public void archiveApp_callerPackageNameIncorrect() {
+        Exception e = assertThrows(
+                SecurityException.class,
+                () -> mArchiveManager.archiveApp(PACKAGE, "different", USER_ID, mIntentSender)
+        );
+        assertThat(e).hasMessageThat().isEqualTo(
+                String.format(
+                        "The callerPackageName %s set by the caller doesn't match the "
+                                + "caller's own package name %s.",
+                        "different",
+                        CALLER_PACKAGE));
+    }
+
+    @Test
+    public void archiveApp_packageNotInstalled() {
+        when(mComputer.getPackageStateInternal(eq(PACKAGE))).thenReturn(null);
+
+        Exception e = assertThrows(
+                PackageManager.NameNotFoundException.class,
+                () -> mArchiveManager.archiveApp(PACKAGE, CALLER_PACKAGE, USER_ID, mIntentSender)
+        );
+        assertThat(e).hasMessageThat().isEqualTo(String.format("Package %s not found.", PACKAGE));
+    }
+
+    @Test
+    public void archiveApp_callerNotInstallerOfRecord() {
+        InstallSource otherInstallSource =
+                InstallSource.create(
+                        CALLER_PACKAGE,
+                        CALLER_PACKAGE,
+                        /* installerPackageName= */ "different",
+                        Binder.getCallingUid(),
+                        CALLER_PACKAGE,
+                        /* installerAttributionTag= */ null,
+                        /* packageSource= */ 0);
+        when(mPackageState.getInstallSource()).thenReturn(otherInstallSource);
+
+        Exception e = assertThrows(
+                SecurityException.class,
+                () -> mArchiveManager.archiveApp(PACKAGE, CALLER_PACKAGE, USER_ID, mIntentSender)
+        );
+        assertThat(e).hasMessageThat().isEqualTo(
+                String.format("Caller is not the installer of record for %s.", PACKAGE));
+    }
+
+    @Test
+    public void archiveApp_callerNotUpdateOwner() {
+        InstallSource otherInstallSource =
+                InstallSource.create(
+                        CALLER_PACKAGE,
+                        CALLER_PACKAGE,
+                        CALLER_PACKAGE,
+                        Binder.getCallingUid(),
+                        /* updateOwnerPackageName= */ "different",
+                        /* installerAttributionTag= */ null,
+                        /* packageSource= */ 0);
+        when(mPackageState.getInstallSource()).thenReturn(otherInstallSource);
+
+        Exception e = assertThrows(
+                SecurityException.class,
+                () -> mArchiveManager.archiveApp(PACKAGE, CALLER_PACKAGE, USER_ID, mIntentSender)
+        );
+        assertThat(e).hasMessageThat().isEqualTo(
+                String.format("Caller is not the update owner for %s.", PACKAGE));
+    }
+}