Add an install flag to force non-staged APEX update

This is a development only feature to speed up development workflow for
teams that have their code package in an APEX.

Bug: 290750901
Test: m
Test: atest ApexManagerTest
Test: see other change in the topic
Merged-In: Ic0abb9e97d529910805af50208baf42b9b28b171
Change-Id: Ib452dcdbdd30991f60205f94be25389379153183
diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
index d738d9e..db4a684 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -1561,6 +1561,14 @@
      */
     public static final int INSTALL_BYPASS_LOW_TARGET_SDK_BLOCK = 0x01000000;
 
+    /**
+     * Flag parameter for {@link #installPackage} to force a non-staged update of an APEX. This is
+     * a development-only feature and should not be used on end user devices.
+     *
+     * @hide
+     */
+    public static final int INSTALL_FORCE_NON_STAGED_APEX_UPDATE = 0x02000000;
+
     /** @hide */
     @IntDef(flag = true, value = {
             DONT_KILL_APP,
diff --git a/services/core/java/com/android/server/pm/ApexManager.java b/services/core/java/com/android/server/pm/ApexManager.java
index 1a6155b..a085b95 100644
--- a/services/core/java/com/android/server/pm/ApexManager.java
+++ b/services/core/java/com/android/server/pm/ApexManager.java
@@ -424,7 +424,7 @@
     /**
      * Performs a non-staged install of the given {@code apexFile}.
      */
-    abstract void installPackage(File apexFile, PackageParser2 packageParser)
+    abstract void installPackage(File apexFile, PackageParser2 packageParser, boolean force)
             throws PackageManagerException;
 
     /**
@@ -1136,7 +1136,7 @@
         }
 
         @Override
-        void installPackage(File apexFile, PackageParser2 packageParser)
+        void installPackage(File apexFile, PackageParser2 packageParser, boolean force)
                 throws PackageManagerException {
             try {
                 final int flags = PackageManager.GET_META_DATA
@@ -1159,7 +1159,7 @@
                 }
                 checkApexSignature(existingApexPkg, newApexPkg);
                 ApexInfo apexInfo = waitForApexService().installAndActivatePackage(
-                        apexFile.getAbsolutePath());
+                        apexFile.getAbsolutePath(), force);
                 final ParsedPackage parsedPackage2 = packageParser.parsePackage(
                         new File(apexInfo.modulePath), flags, /* useCaches= */ false);
                 final PackageInfo finalApexPkg = PackageInfoWithoutStateUtils.generate(
@@ -1505,7 +1505,7 @@
         }
 
         @Override
-        void installPackage(File apexFile, PackageParser2 packageParser) {
+        void installPackage(File apexFile, PackageParser2 packageParser, boolean force) {
             throw new UnsupportedOperationException("APEX updates are not supported");
         }
 
diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java
index 293f7e9..703ae11 100644
--- a/services/core/java/com/android/server/pm/InstallPackageHelper.java
+++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java
@@ -872,8 +872,10 @@
                         "Expected exactly one .apex file under " + dir.getAbsolutePath()
                                 + " got: " + apexes.length);
             }
+            boolean force = (request.mArgs.mInstallFlags
+                    & PackageManager.INSTALL_FORCE_NON_STAGED_APEX_UPDATE) != 0;
             try (PackageParser2 packageParser = mPm.mInjector.getScanningPackageParser()) {
-                mApexManager.installPackage(apexes[0], packageParser);
+                mApexManager.installPackage(apexes[0], packageParser, force);
             }
         } catch (PackageManagerException e) {
             request.mInstallResult.setError("APEX installation failed", e);
diff --git a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
index 9080d46..38b79e5 100644
--- a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
+++ b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
@@ -3208,6 +3208,7 @@
         }
         if (forceNonStaged) {
             sessionParams.isStaged = false;
+            sessionParams.installFlags |= PackageManager.INSTALL_FORCE_NON_STAGED_APEX_UPDATE;
         } else if (staged) {
             sessionParams.setStaged();
         }
diff --git a/services/tests/servicestests/src/com/android/server/pm/ApexManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/ApexManagerTest.java
index ab292ab..872e438 100644
--- a/services/tests/servicestests/src/com/android/server/pm/ApexManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/ApexManagerTest.java
@@ -19,6 +19,7 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.doAnswer;
@@ -362,7 +363,7 @@
 
         File apex = extractResource("test.apex_rebootless_v1", "test.rebootless_apex_v1.apex");
         PackageManagerException e = expectThrows(PackageManagerException.class,
-                () -> mApexManager.installPackage(apex, mPackageParser2));
+                () -> mApexManager.installPackage(apex, mPackageParser2, /* force= */ false));
         assertThat(e).hasMessageThat().contains("It is forbidden to install new APEX packages");
     }
 
@@ -378,10 +379,11 @@
         File finalApex = extractResource("test.rebootles_apex_v2", "test.rebootless_apex_v2.apex");
         ApexInfo newApexInfo = createApexInfo("test.apex_rebootless", 2, /* isActive= */ true,
                 /* isFactory= */ false, finalApex);
-        when(mApexService.installAndActivatePackage(anyString())).thenReturn(newApexInfo);
+        when(mApexService.installAndActivatePackage(anyString(), anyBoolean())).thenReturn(
+                newApexInfo);
 
         File installedApex = extractResource("installed", "test.rebootless_apex_v2.apex");
-        mApexManager.installPackage(installedApex, mPackageParser2);
+        mApexManager.installPackage(installedApex, mPackageParser2, /* force= */ false);
 
         PackageInfo newInfo = mApexManager.getPackageInfo("test.apex.rebootless",
                 ApexManager.MATCH_ACTIVE_PACKAGE);
@@ -416,10 +418,11 @@
         File finalApex = extractResource("test.rebootles_apex_v2", "test.rebootless_apex_v2.apex");
         ApexInfo newApexInfo = createApexInfo("test.apex_rebootless", 2, /* isActive= */ true,
                 /* isFactory= */ false, finalApex);
-        when(mApexService.installAndActivatePackage(anyString())).thenReturn(newApexInfo);
+        when(mApexService.installAndActivatePackage(anyString(), anyBoolean())).thenReturn(
+                newApexInfo);
 
         File installedApex = extractResource("installed", "test.rebootless_apex_v2.apex");
-        mApexManager.installPackage(installedApex, mPackageParser2);
+        mApexManager.installPackage(installedApex, mPackageParser2, /* force= */ false);
 
         PackageInfo newInfo = mApexManager.getPackageInfo("test.apex.rebootless",
                 ApexManager.MATCH_ACTIVE_PACKAGE);
@@ -447,13 +450,14 @@
         mApexManager.scanApexPackagesTraced(mPackageParser2,
                 ParallelPackageParser.makeExecutorService());
 
-        when(mApexService.installAndActivatePackage(anyString())).thenThrow(
+        when(mApexService.installAndActivatePackage(anyString(), anyBoolean())).thenThrow(
                 new RuntimeException("install failed :("));
 
         File installedApex = extractResource("test.apex_rebootless_v1",
                 "test.rebootless_apex_v1.apex");
         assertThrows(PackageManagerException.class,
-                () -> mApexManager.installPackage(installedApex, mPackageParser2));
+                () -> mApexManager.installPackage(installedApex, mPackageParser2, /* force= */
+                        false));
     }
 
     @Test
@@ -468,7 +472,8 @@
         File installedApex = extractResource("shim_different_certificate",
                 "com.android.apex.cts.shim.v2_different_certificate.apex");
         PackageManagerException e = expectThrows(PackageManagerException.class,
-                () -> mApexManager.installPackage(installedApex, mPackageParser2));
+                () -> mApexManager.installPackage(installedApex, mPackageParser2, /* force= */
+                        false));
         assertThat(e).hasMessageThat().contains("APK container signature of ");
         assertThat(e).hasMessageThat().contains(
                 "is not compatible with currently installed on device");
@@ -486,7 +491,8 @@
         File installedApex = extractResource("shim_unsigned_apk_container",
                 "com.android.apex.cts.shim.v2_unsigned_apk_container.apex");
         PackageManagerException e = expectThrows(PackageManagerException.class,
-                () -> mApexManager.installPackage(installedApex, mPackageParser2));
+                () -> mApexManager.installPackage(installedApex, mPackageParser2, /* force= */
+                        false));
         assertThat(e).hasMessageThat().contains("Failed to collect certificates from ");
     }