Refactor VM lifecycle routine to a separate class

Bug: N/A
Test: N/A
Change-Id: Ib30cf12a6238b85a1b80a9a010e93680bead9df7
diff --git a/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/MainActivity.java b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/MainActivity.java
index 82331b3..376cc7f 100644
--- a/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/MainActivity.java
+++ b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/MainActivity.java
@@ -29,10 +29,8 @@
 import android.os.ServiceManager;
 import android.system.virtualizationservice_internal.IVirtualizationServiceInternal;
 import android.system.virtualmachine.VirtualMachine;
-import android.system.virtualmachine.VirtualMachineCallback;
 import android.system.virtualmachine.VirtualMachineConfig;
 import android.system.virtualmachine.VirtualMachineException;
-import android.system.virtualmachine.VirtualMachineManager;
 import android.util.Log;
 import android.view.SurfaceView;
 import android.view.View;
@@ -56,7 +54,8 @@
 
 public class MainActivity extends Activity {
     static final String TAG = "VmLauncherApp";
-    private static final String VM_NAME = "my_custom_vm";
+    // TODO: this path should be from outside of this activity
+    private static final String VM_CONFIG_PATH = "/data/local/tmp/vm_config.json";
 
     private static final boolean DEBUG = true;
     private static final int RECORD_AUDIO_PERMISSION_REQUEST_CODE = 101;
@@ -83,65 +82,25 @@
         mExecutorService = Executors.newCachedThreadPool();
         getWindow().setDecorFitsSystemWindows(false);
         setContentView(R.layout.activity_main);
-        VirtualMachineCallback callback =
-                new VirtualMachineCallback() {
-                    // store reference to ExecutorService to avoid race condition
-                    private final ExecutorService mService = mExecutorService;
 
-                    @Override
-                    public void onPayloadStarted(VirtualMachine vm) {
-                        // This event is only from Microdroid-based VM. Custom VM shouldn't emit
-                        // this.
-                    }
+        ConfigJson json = ConfigJson.from(VM_CONFIG_PATH);
+        VirtualMachineConfig config = json.toConfig(this);
 
-                    @Override
-                    public void onPayloadReady(VirtualMachine vm) {
-                        // This event is only from Microdroid-based VM. Custom VM shouldn't emit
-                        // this.
-                    }
-
-                    @Override
-                    public void onPayloadFinished(VirtualMachine vm, int exitCode) {
-                        // This event is only from Microdroid-based VM. Custom VM shouldn't emit
-                        // this.
-                    }
-
-                    @Override
-                    public void onError(VirtualMachine vm, int errorCode, String message) {
-                        Log.e(TAG, "Error from VM. code: " + errorCode + " (" + message + ")");
-                        setResult(RESULT_CANCELED);
-                        finish();
-                    }
-
-                    @Override
-                    public void onStopped(VirtualMachine vm, int reason) {
-                        Log.d(TAG, "VM stopped. Reason: " + reason);
-                        setResult(RESULT_OK);
-                        finish();
-                    }
-                };
+        Runner runner;
+        try {
+            runner = Runner.create(this, config);
+        } catch (VirtualMachineException e) {
+            throw new RuntimeException(e);
+        }
+        mVirtualMachine = runner.getVm();
+        runner.getExitStatus()
+                .thenAcceptAsync(
+                        success -> {
+                            setResult(success ? RESULT_OK : RESULT_CANCELED);
+                            finish();
+                        });
 
         try {
-            VirtualMachineConfig config =
-                    ConfigJson.from("/data/local/tmp/vm_config.json").toConfig(this);
-            VirtualMachineManager vmm =
-                    getApplication().getSystemService(VirtualMachineManager.class);
-            if (vmm == null) {
-                Log.e(TAG, "vmm is null");
-                return;
-            }
-            mVirtualMachine = vmm.getOrCreate(VM_NAME, config);
-            try {
-                mVirtualMachine.setConfig(config);
-            } catch (VirtualMachineException e) {
-                vmm.delete(VM_NAME);
-                mVirtualMachine = vmm.create(VM_NAME, config);
-                Log.e(TAG, "error for setting VM config", e);
-            }
-
-            Log.d(TAG, "vm start");
-            mVirtualMachine.run();
-            mVirtualMachine.setCallback(Executors.newSingleThreadExecutor(), callback);
             if (DEBUG) {
                 InputStream console = mVirtualMachine.getConsoleOutput();
                 InputStream log = mVirtualMachine.getLogOutput();
diff --git a/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/Runner.java b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/Runner.java
new file mode 100644
index 0000000..f50ec86
--- /dev/null
+++ b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/Runner.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2024 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.virtualization.vmlauncher;
+
+import android.content.Context;
+import android.system.virtualmachine.VirtualMachine;
+import android.system.virtualmachine.VirtualMachineCallback;
+import android.system.virtualmachine.VirtualMachineConfig;
+import android.system.virtualmachine.VirtualMachineCustomImageConfig;
+import android.system.virtualmachine.VirtualMachineException;
+import android.system.virtualmachine.VirtualMachineManager;
+import android.util.Log;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ForkJoinPool;
+
+/** Utility class for creating a VM and waiting for it to finish. */
+class Runner {
+    private static final String TAG = MainActivity.TAG;
+    private final VirtualMachine mVirtualMachine;
+    private final Callback mCallback;
+
+    private Runner(VirtualMachine vm, Callback cb) {
+        mVirtualMachine = vm;
+        mCallback = cb;
+    }
+
+    /** Create a virtual machine of the given config, under the given context. */
+    static Runner create(Context context, VirtualMachineConfig config)
+            throws VirtualMachineException {
+        VirtualMachineManager vmm = context.getSystemService(VirtualMachineManager.class);
+        VirtualMachineCustomImageConfig customConfig = config.getCustomImageConfig();
+        if (customConfig == null) {
+            throw new RuntimeException("CustomImageConfig is missing");
+        }
+
+        String name = customConfig.getName();
+        if (name == null || name.isEmpty()) {
+            throw new RuntimeException("Virtual machine's name is missing in the config");
+        }
+
+        VirtualMachine vm = vmm.getOrCreate(name, config);
+        try {
+            vm.setConfig(config);
+        } catch (VirtualMachineException e) {
+            vmm.delete(name);
+            vm = vmm.create(name, config);
+            Log.w(TAG, "Re-creating virtual machine (" + name + ")", e);
+        }
+
+        Callback cb = new Callback();
+        vm.setCallback(ForkJoinPool.commonPool(), cb);
+        vm.run();
+        return new Runner(vm, cb);
+    }
+
+    /** Give access to the underlying VirtualMachine object. */
+    VirtualMachine getVm() {
+        return mVirtualMachine;
+    }
+
+    /** Get future about VM's exit status. */
+    CompletableFuture<Boolean> getExitStatus() {
+        return mCallback.mFinishedSuccessfully;
+    }
+
+    private static class Callback implements VirtualMachineCallback {
+        final CompletableFuture<Boolean> mFinishedSuccessfully = new CompletableFuture<>();
+
+        @Override
+        public void onPayloadStarted(VirtualMachine vm) {
+            // This event is only from Microdroid-based VM. Custom VM shouldn't emit this.
+        }
+
+        @Override
+        public void onPayloadReady(VirtualMachine vm) {
+            // This event is only from Microdroid-based VM. Custom VM shouldn't emit this.
+        }
+
+        @Override
+        public void onPayloadFinished(VirtualMachine vm, int exitCode) {
+            // This event is only from Microdroid-based VM. Custom VM shouldn't emit this.
+        }
+
+        @Override
+        public void onError(VirtualMachine vm, int errorCode, String message) {
+            Log.e(TAG, "Error from VM. code: " + errorCode + " (" + message + ")");
+            mFinishedSuccessfully.complete(false);
+        }
+
+        @Override
+        public void onStopped(VirtualMachine vm, int reason) {
+            Log.d(TAG, "VM stopped. Reason: " + reason);
+            mFinishedSuccessfully.complete(true);
+        }
+    }
+}