Merge "Allow setting APK path explicitly"
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java b/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
index d8ff8c6..9555d1e 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
@@ -18,6 +18,8 @@
 
 import static android.os.ParcelFileDescriptor.MODE_READ_ONLY;
 
+import static java.util.Objects.requireNonNull;
+
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -57,7 +59,7 @@
     private static final String KEY_MEMORY_MIB = "memoryMib";
     private static final String KEY_NUM_CPUS = "numCpus";
 
-    // Paths to the APK file of this application.
+    // Absolute path to the APK file containing the VM payload.
     @NonNull private final String mApkPath;
 
     /** @hide */
@@ -128,7 +130,11 @@
             boolean protectedVm,
             int memoryMib,
             int numCpus) {
-        mApkPath = Objects.requireNonNull(apkPath);
+        requireNonNull(apkPath);
+        if (!apkPath.startsWith("/")) {
+            throw new IllegalArgumentException("APK path must be an absolute path");
+        }
+        mApkPath = apkPath;
         mPayloadConfigPath = payloadConfigPath;
         mPayloadBinaryPath = payloadBinaryPath;
         mDebugLevel = debugLevel;
@@ -188,6 +194,17 @@
     }
 
     /**
+     * Returns the absolute path of the APK which should contain the binary payload that will
+     * execute within the VM.
+     *
+     * @hide
+     */
+    @NonNull
+    public String getApkPath() {
+        return mApkPath;
+    }
+
+    /**
      * Returns the path to the payload config within the owning application.
      *
      * @hide
@@ -307,6 +324,7 @@
      */
     public static final class Builder {
         private final Context mContext;
+        @Nullable private String mApkPath;
         @Nullable private String mPayloadConfigPath;
         @Nullable private String mPayloadBinaryPath;
         @DebugLevel private int mDebugLevel;
@@ -321,7 +339,7 @@
          * @hide
          */
         public Builder(@NonNull Context context) {
-            mContext = Objects.requireNonNull(context);
+            mContext = requireNonNull(context);
             mDebugLevel = DEBUG_LEVEL_NONE;
             mNumCpus = 1;
         }
@@ -333,9 +351,9 @@
          */
         @NonNull
         public VirtualMachineConfig build() {
-            final String apkPath = mContext.getPackageCodePath();
+            String apkPath = (mApkPath == null) ? mContext.getPackageCodePath() : mApkPath;
 
-            final int availableCpus = Runtime.getRuntime().availableProcessors();
+            int availableCpus = Runtime.getRuntime().availableProcessors();
             if (mNumCpus < 1 || mNumCpus > availableCpus) {
                 throw new IllegalArgumentException("Number of vCPUs (" + mNumCpus + ") is out of "
                         + "range [1, " + availableCpus + "]");
@@ -372,6 +390,18 @@
         }
 
         /**
+         * Sets the absolute path of the APK containing the binary payload that will execute within
+         * the VM. If not set explicitly, defaults to the primary APK of the context.
+         *
+         * @hide
+         */
+        @NonNull
+        public Builder setApkPath(@NonNull String apkPath) {
+            mApkPath = requireNonNull(apkPath);
+            return this;
+        }
+
+        /**
          * Sets the path within the APK to the payload config file that defines software aspects
          * of the VM.
          *
@@ -380,7 +410,7 @@
         @RequiresPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION)
         @NonNull
         public Builder setPayloadConfigPath(@NonNull String payloadConfigPath) {
-            mPayloadConfigPath = Objects.requireNonNull(payloadConfigPath);
+            mPayloadConfigPath = requireNonNull(payloadConfigPath);
             return this;
         }
 
@@ -392,7 +422,7 @@
          */
         @NonNull
         public Builder setPayloadBinaryPath(@NonNull String payloadBinaryPath) {
-            mPayloadBinaryPath = Objects.requireNonNull(payloadBinaryPath);
+            mPayloadBinaryPath = requireNonNull(payloadBinaryPath);
             return this;
         }
 
diff --git a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
index c4296df..e5052bf 100644
--- a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
+++ b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
@@ -158,6 +158,39 @@
                 .contains("android.permission.USE_CUSTOM_VIRTUAL_MACHINE permission");
     }
 
+    @Test
+    @CddTest(requirements = {
+            "9.17/C-1-1",
+    })
+    public void validApkPathIsAccepted() throws Exception {
+        assumeSupportedKernel();
+
+        VirtualMachineConfig config = mInner.newVmConfigBuilder()
+                .setPayloadBinaryPath("MicrodroidTestNativeLib.so")
+                .setApkPath(getContext().getPackageCodePath())
+                .setMemoryMib(minMemoryRequired())
+                .build();
+
+        VirtualMachine vm = mInner.forceCreateNewVirtualMachine(
+                "test_vm_explicit_apk_path", config);
+
+        TestResults testResults = runVmTestService(vm);
+        assertThat(testResults.mException).isNull();
+    }
+
+    @Test
+    @CddTest(requirements = {
+            "9.17/C-1-1",
+    })
+    public void invalidApkPathIsRejected() {
+        assumeSupportedKernel();
+
+        VirtualMachineConfig.Builder builder = mInner.newVmConfigBuilder()
+                .setPayloadBinaryPath("MicrodroidTestNativeLib.so")
+                .setApkPath("relative/path/to.apk")
+                .setMemoryMib(minMemoryRequired());
+        assertThrows(IllegalArgumentException.class, () -> builder.build());
+    }
 
     @Test
     @CddTest(requirements = {