Add basic test that shares a VM from one app to another

This test consist of 2 apps:
* MicrodroidTestApp - test driver
* MicrodroidVmShareApp - helper app that exposes a service that
  MicrodroidTestApp binds to.

The test orchestarted by MicrodroidTestApp consists of the following:

1. MicrodroidTestApp starts & stops a VM
2. MicrodroidTestApp creates a descriptor of that VM
3. MicrodroidTestApp binds to service in MicrodroidVmShareApp
4. MicrodroidTestApp sends the descriptor to MicrodroidVmShareApp
5. MicrodroidVmShareApp starts a VM from that descriptor, and connects
   to the vsock service exposed by the VM.
6. MicrodroidVmShareApp shares a binder interface for MicrodroidTestApp
   to interact with the service exposed by the MicrodroidVmShareApp VM.
7. MicrodroidTestApp uses that binder to assert on the VM.

This change adds the scaffolding and a basic test, more involved test
(e.g. with trusted storage) will be added in the follow up changes.

Bug: 259384440
Test: atest MicrodroidTestApp
Change-Id: I924c0fd9494010fd55fd9062206e8f3202e43b5b
diff --git a/tests/testapk/Android.bp b/tests/testapk/Android.bp
index 3c487ee..bafab53 100644
--- a/tests/testapk/Android.bp
+++ b/tests/testapk/Android.bp
@@ -8,6 +8,10 @@
         "cts",
         "general-tests",
     ],
+    static_libs: [
+        "com.android.microdroid.testservice-java",
+        "com.android.microdroid.test.vmshare_service-java",
+    ],
     sdk_version: "test_current",
     jni_uses_platform_apis: true,
     use_embedded_native_libs: true,
@@ -25,7 +29,6 @@
         "androidx.test.ext.junit",
         "authfs_test_apk_assets",
         "cbor-java",
-        "com.android.microdroid.testservice-java",
         "truth-prebuilt",
         "compatibility-common-util-devicesidelib",
     ],
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 926dd77..13d56e1 100644
--- a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
+++ b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
@@ -24,15 +24,20 @@
 import static android.system.virtualmachine.VirtualMachineManager.CAPABILITY_PROTECTED_VM;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.common.truth.TruthJUnit.assume;
 
 import static org.junit.Assert.assertThrows;
 
 import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
 
+import android.content.ComponentName;
 import android.content.Context;
 import android.content.ContextWrapper;
+import android.content.Intent;
+import android.content.ServiceConnection;
 import android.os.Build;
+import android.os.IBinder;
 import android.os.Parcel;
 import android.os.ParcelFileDescriptor;
 import android.os.ParcelFileDescriptor.AutoCloseInputStream;
@@ -50,6 +55,7 @@
 
 import com.android.compatibility.common.util.CddTest;
 import com.android.microdroid.test.device.MicrodroidDeviceTestBase;
+import com.android.microdroid.test.vmshare.IVmShareTestService;
 import com.android.microdroid.testservice.ITestService;
 
 import com.google.common.base.Strings;
@@ -86,6 +92,8 @@
 import java.util.OptionalLong;
 import java.util.UUID;
 import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicReference;
 
 import co.nstant.in.cbor.CborDecoder;
@@ -1633,6 +1641,76 @@
         assertThat(testResults.mFileContent).isEqualTo("not a secret!");
     }
 
+    @Test
+    public void testShareVmWithAnotherApp() throws Exception {
+        assumeSupportedKernel();
+
+        Context ctx = getContext();
+        Context otherAppCtx = ctx.createPackageContext(VM_SHARE_APP_PACKAGE_NAME, 0);
+
+        VirtualMachineConfig config =
+                new VirtualMachineConfig.Builder(otherAppCtx)
+                        .setDebugLevel(DEBUG_LEVEL_FULL)
+                        .setProtectedVm(isProtectedVm())
+                        .setPayloadBinaryName("MicrodroidPayloadInOtherAppNativeLib.so")
+                        .build();
+
+        VirtualMachine vm = forceCreateNewVirtualMachine("vm_to_share", config);
+        // Just start & stop the VM.
+        runVmTestService(vm, (ts, tr) -> {});
+        // Get a descriptor that we will share with another app (VM_SHARE_APP_PACKAGE_NAME)
+        VirtualMachineDescriptor vmDesc = vm.toDescriptor();
+
+        Intent serviceIntent = new Intent();
+        serviceIntent.setComponent(
+                new ComponentName(
+                        VM_SHARE_APP_PACKAGE_NAME,
+                        "com.android.microdroid.test.sharevm.VmShareServiceImpl"));
+
+        VmShareServiceConnection connection = new VmShareServiceConnection();
+        boolean ret = ctx.bindService(serviceIntent, connection, Context.BIND_AUTO_CREATE);
+        assertWithMessage("Failed to bind to " + serviceIntent).that(ret).isTrue();
+
+        IVmShareTestService service = connection.waitForService();
+        assertWithMessage("Timed out connecting to " + serviceIntent).that(service).isNotNull();
+
+        try {
+            // Send the VM descriptor to the other app. When received, it will reconstruct the VM
+            // from the descriptor, start it, connect to the ITestService in it, creates a "proxy"
+            // ITestService binder that delegates all the calls to the VM, and share it with this
+            // app. It will allow us to verify assertions on the running VM in the other app.
+            ITestService testServiceProxy = service.startVm(vmDesc);
+
+            int result = testServiceProxy.addInteger(37, 73);
+            assertThat(result).isEqualTo(110);
+        } finally {
+            ctx.unbindService(connection);
+        }
+    }
+
+    private static class VmShareServiceConnection implements ServiceConnection {
+
+        private final CountDownLatch mLatch = new CountDownLatch(1);
+
+        private IVmShareTestService mVmShareTestService;
+
+        @Override
+        public void onServiceConnected(ComponentName name, IBinder service) {
+            mVmShareTestService = IVmShareTestService.Stub.asInterface(service);
+            mLatch.countDown();
+        }
+
+        @Override
+        public void onServiceDisconnected(ComponentName name) {}
+
+        private IVmShareTestService waitForService() throws Exception {
+            if (!mLatch.await(1, TimeUnit.MINUTES)) {
+                return null;
+            }
+            return mVmShareTestService;
+        }
+    }
+
     private VirtualMachineDescriptor toParcelFromParcel(VirtualMachineDescriptor descriptor) {
         Parcel parcel = Parcel.obtain();
         descriptor.writeToParcel(parcel, 0);
diff --git a/tests/vmshareapp/Android.bp b/tests/vmshareapp/Android.bp
index 2b117a1..6c2c9e4 100644
--- a/tests/vmshareapp/Android.bp
+++ b/tests/vmshareapp/Android.bp
@@ -5,6 +5,7 @@
 // Helper app to verify that we can create a VM using others app payload, and share VMs between apps
 android_test_helper_app {
     name: "MicrodroidVmShareApp",
+    srcs: ["src/java/**/*.java"],
     // Defaults are defined in ../testapk/Android.bp
     defaults: ["MicrodroidTestAppsDefaults"],
     jni_libs: [
diff --git a/tests/vmshareapp/AndroidManifest.xml b/tests/vmshareapp/AndroidManifest.xml
index eed3364..b623f7f 100644
--- a/tests/vmshareapp/AndroidManifest.xml
+++ b/tests/vmshareapp/AndroidManifest.xml
@@ -20,5 +20,13 @@
     <uses-feature android:name="android.software.virtualization_framework"
                   android:required="false" />
 
-    <application />
+    <application>
+        <service android:name="com.android.microdroid.test.sharevm.VmShareServiceImpl"
+                 android:exported="true">
+            <intent-filter>
+                <action android:name="com.android.microdroid.test.sharevm.VmShareService"/>
+            </intent-filter>
+        </service>
+    </application>
+
 </manifest>
diff --git a/tests/vmshareapp/aidl/Android.bp b/tests/vmshareapp/aidl/Android.bp
new file mode 100644
index 0000000..df4a4b4
--- /dev/null
+++ b/tests/vmshareapp/aidl/Android.bp
@@ -0,0 +1,15 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+// Unfortunatelly aidl_interface doesn't work well with .aidl files that depend on java-only
+// parcelables (e.g. Bundle, VirtualMachineDescriptor), hence this java_library.
+java_library {
+    name: "com.android.microdroid.test.vmshare_service-java",
+    srcs: ["com/**/*.aidl"],
+    sdk_version: "test_current",
+    static_libs: ["com.android.microdroid.testservice-java"],
+    aidl: {
+        include_dirs: ["packages/modules/Virtualization/tests/aidl/"],
+    },
+}
diff --git a/tests/vmshareapp/aidl/com/android/microdroid/test/vmshare/IVmShareTestService.aidl b/tests/vmshareapp/aidl/com/android/microdroid/test/vmshare/IVmShareTestService.aidl
new file mode 100644
index 0000000..fe6ca43
--- /dev/null
+++ b/tests/vmshareapp/aidl/com/android/microdroid/test/vmshare/IVmShareTestService.aidl
@@ -0,0 +1,24 @@
+/*
+ * Copyright 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.microdroid.test.vmshare;
+
+import android.system.virtualmachine.VirtualMachineDescriptor;
+import com.android.microdroid.testservice.ITestService;
+
+/** {@hide} */
+interface IVmShareTestService {
+    ITestService startVm(in VirtualMachineDescriptor vmDesc);
+}
diff --git a/tests/vmshareapp/src/java/com/android/microdroid/test/sharevm/VmShareServiceImpl.java b/tests/vmshareapp/src/java/com/android/microdroid/test/sharevm/VmShareServiceImpl.java
new file mode 100644
index 0000000..215bc6d
--- /dev/null
+++ b/tests/vmshareapp/src/java/com/android/microdroid/test/sharevm/VmShareServiceImpl.java
@@ -0,0 +1,238 @@
+/*
+ * 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.microdroid.test.sharevm;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.system.virtualmachine.VirtualMachine;
+import android.system.virtualmachine.VirtualMachineCallback;
+import android.system.virtualmachine.VirtualMachineDescriptor;
+import android.system.virtualmachine.VirtualMachineException;
+import android.system.virtualmachine.VirtualMachineManager;
+import android.util.Log;
+
+import com.android.microdroid.test.vmshare.IVmShareTestService;
+import com.android.microdroid.testservice.ITestService;
+
+import java.util.UUID;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * A {@link Service} that is used in end-to-end tests of the {@link VirtualMachine} sharing
+ * functionality.
+ *
+ * <p>During the test {@link com.android.microdroid.test.MicrodroidTests} will bind to this service,
+ * and call {@link #startVm(VirtualMachineDescriptor)} to share the VM. This service then will
+ * create a {@link VirtualMachine} from that descriptor, {@link VirtualMachine#run() run} it, and
+ * send back {@link RemoteTestServiceDelegate}. The {@code MicrodroidTests} can use that {@link
+ * RemoteTestServiceDelegate} to assert conditions on the VM running in the {@link
+ * VmShareServiceImpl}.
+ *
+ * <p>The {@link VirtualMachine} running in this service will be stopped on {@link
+ * #onUnbind(Intent)}.
+ *
+ * @see com.android.microdroid.test.MicrodroidTests#testShareVmWithAnotherApp
+ */
+public class VmShareServiceImpl extends Service {
+
+    private static final String TAG = "VmShareApp";
+
+    private IVmShareTestService.Stub mBinder;
+
+    private VirtualMachine mVirtualMachine;
+
+    @Override
+    public void onCreate() {
+        mBinder = new ServiceImpl();
+    }
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        Log.i(TAG, "onBind " + intent + " binder = " + mBinder);
+        return mBinder;
+    }
+
+    @Override
+    public boolean onUnbind(Intent intent) {
+        deleteVm();
+        // Tell framework that it shouldn't call onRebind.
+        return false;
+    }
+
+    private void deleteVm() {
+        if (mVirtualMachine == null) {
+            return;
+        }
+        try {
+            mVirtualMachine.stop();
+            String name = mVirtualMachine.getName();
+            VirtualMachineManager vmm = getSystemService(VirtualMachineManager.class);
+            vmm.delete(name);
+            mVirtualMachine = null;
+        } catch (VirtualMachineException e) {
+            Log.e(TAG, "Failed to stop " + mVirtualMachine, e);
+        }
+    }
+
+    public ITestService startVm(VirtualMachineDescriptor vmDesc) throws Exception {
+        // Cleanup VM left from the previous test.
+        deleteVm();
+
+        VirtualMachineManager vmm = getSystemService(VirtualMachineManager.class);
+
+        // Add random uuid to make sure that different tests that bind to this service don't trip
+        // over each other.
+        String vmName = "imported_vm" + UUID.randomUUID();
+
+        final CountDownLatch latch = new CountDownLatch(1);
+        VirtualMachineCallback callback =
+                new VirtualMachineCallback() {
+
+                    @Override
+                    public void onPayloadStarted(VirtualMachine vm) {
+                        // Ignored
+                    }
+
+                    @Override
+                    public void onPayloadReady(VirtualMachine vm) {
+                        latch.countDown();
+                    }
+
+                    @Override
+                    public void onPayloadFinished(VirtualMachine vm, int exitCode) {
+                        // Ignored
+                    }
+
+                    @Override
+                    public void onError(VirtualMachine vm, int errorCode, String message) {
+                        throw new RuntimeException(
+                                "VM failed with error " + errorCode + " : " + message);
+                    }
+
+                    @Override
+                    public void onStopped(VirtualMachine vm, int reason) {
+                        // Ignored
+                    }
+                };
+
+        mVirtualMachine = vmm.importFromDescriptor(vmName, vmDesc);
+        mVirtualMachine.setCallback(getMainExecutor(), callback);
+
+        Log.i(TAG, "Starting VM " + vmName);
+        mVirtualMachine.run();
+        if (!latch.await(1, TimeUnit.MINUTES)) {
+            throw new TimeoutException("Timed out starting VM");
+        }
+
+        Log.i(
+                TAG,
+                "Payload is ready, connecting to the vsock service at port "
+                        + ITestService.SERVICE_PORT);
+        ITestService testService =
+                ITestService.Stub.asInterface(
+                        mVirtualMachine.connectToVsockServer(ITestService.SERVICE_PORT));
+        return new RemoteTestServiceDelegate(testService);
+    }
+
+    final class ServiceImpl extends IVmShareTestService.Stub {
+
+        @Override
+        public ITestService startVm(VirtualMachineDescriptor vmDesc) {
+            Log.i(TAG, "startVm binder call received");
+            try {
+                return VmShareServiceImpl.this.startVm(vmDesc);
+            } catch (Exception e) {
+                Log.e(TAG, "Failed to startVm", e);
+                throw new IllegalStateException("Failed to startVm", e);
+            }
+        }
+    }
+
+    private static class RemoteTestServiceDelegate extends ITestService.Stub {
+
+        private final ITestService mServiceInVm;
+
+        private RemoteTestServiceDelegate(ITestService serviceInVm) {
+            mServiceInVm = serviceInVm;
+        }
+
+        @Override
+        public int addInteger(int a, int b) throws RemoteException {
+            return mServiceInVm.addInteger(a, b);
+        }
+
+        @Override
+        public String readProperty(String prop) throws RemoteException {
+            throw new UnsupportedOperationException("Not supported");
+        }
+
+        @Override
+        public byte[] insecurelyExposeVmInstanceSecret() throws RemoteException {
+            throw new UnsupportedOperationException("Not supported");
+        }
+
+        @Override
+        public byte[] insecurelyExposeAttestationCdi() throws RemoteException {
+            throw new UnsupportedOperationException("Not supported");
+        }
+
+        @Override
+        public byte[] getBcc() throws RemoteException {
+            throw new UnsupportedOperationException("Not supported");
+        }
+
+        @Override
+        public String getApkContentsPath() throws RemoteException {
+            throw new UnsupportedOperationException("Not supported");
+        }
+
+        @Override
+        public String getEncryptedStoragePath() throws RemoteException {
+            throw new UnsupportedOperationException("Not supported");
+        }
+
+        @Override
+        public void runEchoReverseServer() throws RemoteException {
+            throw new UnsupportedOperationException("Not supported");
+        }
+
+        @Override
+        public String[] getEffectiveCapabilities() throws RemoteException {
+            throw new UnsupportedOperationException("Not supported");
+        }
+
+        @Override
+        public void writeToFile(String content, String path) throws RemoteException {
+            throw new UnsupportedOperationException("Not supported");
+        }
+
+        @Override
+        public String readFromFile(String path) throws RemoteException {
+            // TODO(b/259384440): implement for the VM share test including trusted storage.
+            throw new UnsupportedOperationException("Not supported");
+        }
+
+        @Override
+        public void quit() throws RemoteException {
+            throw new UnsupportedOperationException("Not supported");
+        }
+    }
+}