Move VmLauncherService to vm_launcher_lib

In this commit, just move the related code to vm_launcher_lib

Bug: 368281954
Test: run terminal app
Change-Id: I560aefea1efffa97aba9667c1c4ebfce9570153d
diff --git a/libs/vm_launcher_lib/Android.bp b/libs/vm_launcher_lib/Android.bp
index 8591c8d..cb6fc9e 100644
--- a/libs/vm_launcher_lib/Android.bp
+++ b/libs/vm_launcher_lib/Android.bp
@@ -9,5 +9,12 @@
         "//apex_available:platform",
         "com.android.virt",
     ],
-    sdk_version: "system_current",
+    platform_apis: true,
+    static_libs: [
+        "gson",
+    ],
+    libs: [
+        "framework-virtualization.impl",
+        "framework-annotations-lib",
+    ],
 }
diff --git a/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/ConfigJson.java b/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/ConfigJson.java
new file mode 100644
index 0000000..6d39b46
--- /dev/null
+++ b/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/ConfigJson.java
@@ -0,0 +1,260 @@
+/*
+ * 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.graphics.Rect;
+import android.system.virtualmachine.VirtualMachineConfig;
+import android.system.virtualmachine.VirtualMachineCustomImageConfig;
+import android.system.virtualmachine.VirtualMachineCustomImageConfig.AudioConfig;
+import android.system.virtualmachine.VirtualMachineCustomImageConfig.Disk;
+import android.system.virtualmachine.VirtualMachineCustomImageConfig.DisplayConfig;
+import android.system.virtualmachine.VirtualMachineCustomImageConfig.GpuConfig;
+import android.system.virtualmachine.VirtualMachineCustomImageConfig.Partition;
+import android.util.DisplayMetrics;
+import android.view.WindowManager;
+import android.view.WindowMetrics;
+
+import com.google.gson.Gson;
+import com.google.gson.annotations.SerializedName;
+
+import java.io.FileReader;
+import java.util.Arrays;
+
+/** This class and its inner classes model vm_config.json. */
+class ConfigJson {
+    private static final boolean DEBUG = true;
+
+    private ConfigJson() {}
+
+    @SerializedName("protected")
+    private boolean isProtected;
+
+    private String name;
+    private String cpu_topology;
+    private String platform_version;
+    private int memory_mib = 1024;
+    private String console_input_device;
+    private String bootloader;
+    private String kernel;
+    private String initrd;
+    private String params;
+    private boolean debuggable;
+    private boolean console_out;
+    private boolean connect_console;
+    private boolean network;
+    private InputJson input;
+    private AudioJson audio;
+    private DiskJson[] disks;
+    private DisplayJson display;
+    private GpuJson gpu;
+
+    /** Parses JSON file at jsonPath */
+    static ConfigJson from(String jsonPath) {
+        try (FileReader r = new FileReader(jsonPath)) {
+            return new Gson().fromJson(r, ConfigJson.class);
+        } catch (Exception e) {
+            throw new RuntimeException("Failed to parse " + jsonPath, e);
+        }
+    }
+
+    private int getCpuTopology() {
+        switch (cpu_topology) {
+            case "one_cpu":
+                return VirtualMachineConfig.CPU_TOPOLOGY_ONE_CPU;
+            case "match_host":
+                return VirtualMachineConfig.CPU_TOPOLOGY_MATCH_HOST;
+            default:
+                throw new RuntimeException("invalid cpu topology: " + cpu_topology);
+        }
+    }
+
+    private int getDebugLevel() {
+        return debuggable
+                ? VirtualMachineConfig.DEBUG_LEVEL_FULL
+                : VirtualMachineConfig.DEBUG_LEVEL_NONE;
+    }
+
+    /** Converts this parsed JSON into VirtualMachieConfig */
+    VirtualMachineConfig toConfig(Context context) {
+        return new VirtualMachineConfig.Builder(context)
+                .setProtectedVm(isProtected)
+                .setMemoryBytes((long) memory_mib * 1024 * 1024)
+                .setConsoleInputDevice(console_input_device)
+                .setCpuTopology(getCpuTopology())
+                .setCustomImageConfig(toCustomImageConfig(context))
+                .setDebugLevel(getDebugLevel())
+                .setVmOutputCaptured(console_out)
+                .setConnectVmConsole(connect_console)
+                .build();
+    }
+
+    private VirtualMachineCustomImageConfig toCustomImageConfig(Context context) {
+        VirtualMachineCustomImageConfig.Builder builder =
+                new VirtualMachineCustomImageConfig.Builder();
+
+        builder.setName(name)
+                .setBootloaderPath(bootloader)
+                .setKernelPath(kernel)
+                .setInitrdPath(initrd)
+                .useNetwork(network);
+
+        if (input != null) {
+            builder.useTouch(input.touchscreen)
+                    .useKeyboard(input.keyboard)
+                    .useMouse(input.mouse)
+                    .useTrackpad(input.trackpad)
+                    .useSwitches(input.switches);
+        }
+
+        if (audio != null) {
+            builder.setAudioConfig(audio.toConfig());
+        }
+
+        if (display != null) {
+            builder.setDisplayConfig(display.toConfig(context));
+        }
+
+        if (gpu != null) {
+            builder.setGpuConfig(gpu.toConfig());
+        }
+
+        if (params != null) {
+            Arrays.stream(params.split(" ")).forEach(builder::addParam);
+        }
+
+        if (disks != null) {
+            Arrays.stream(disks).map(d -> d.toConfig()).forEach(builder::addDisk);
+        }
+
+        return builder.build();
+    }
+
+    private static class InputJson {
+        private InputJson() {}
+
+        private boolean touchscreen;
+        private boolean keyboard;
+        private boolean mouse;
+        private boolean switches;
+        private boolean trackpad;
+    }
+
+    private static class AudioJson {
+        private AudioJson() {}
+
+        private boolean microphone;
+        private boolean speaker;
+
+        private AudioConfig toConfig() {
+            return new AudioConfig.Builder()
+                    .setUseMicrophone(microphone)
+                    .setUseSpeaker(speaker)
+                    .build();
+        }
+    }
+
+    private static class DiskJson {
+        private DiskJson() {}
+
+        private boolean writable;
+        private String image;
+        private PartitionJson[] partitions;
+
+        private Disk toConfig() {
+            Disk d = writable ? Disk.RWDisk(image) : Disk.RODisk(image);
+            for (PartitionJson pj : partitions) {
+                boolean writable = this.writable && pj.writable;
+                d.addPartition(new Partition(pj.label, pj.path, writable, pj.guid));
+            }
+            return d;
+        }
+    }
+
+    private static class PartitionJson {
+        private PartitionJson() {}
+
+        private boolean writable;
+        private String label;
+        private String path;
+        private String guid;
+    }
+
+    private static class DisplayJson {
+        private DisplayJson() {}
+
+        private float scale;
+        private int refresh_rate;
+        private int width_pixels;
+        private int height_pixels;
+
+        private DisplayConfig toConfig(Context context) {
+            WindowManager wm = context.getSystemService(WindowManager.class);
+            WindowMetrics metrics = wm.getCurrentWindowMetrics();
+            Rect dispBounds = metrics.getBounds();
+
+            int width = width_pixels > 0 ? width_pixels : dispBounds.right;
+            int height = height_pixels > 0 ? height_pixels : dispBounds.bottom;
+
+            int dpi = (int) (DisplayMetrics.DENSITY_DEFAULT * metrics.getDensity());
+            if (scale > 0.0f) {
+                dpi = (int) (dpi * scale);
+            }
+
+            int refreshRate = (int) context.getDisplay().getRefreshRate();
+            if (this.refresh_rate != 0) {
+                refreshRate = this.refresh_rate;
+            }
+
+            return new DisplayConfig.Builder()
+                    .setWidth(width)
+                    .setHeight(height)
+                    .setHorizontalDpi(dpi)
+                    .setVerticalDpi(dpi)
+                    .setRefreshRate(refreshRate)
+                    .build();
+        }
+    }
+
+    private static class GpuJson {
+        private GpuJson() {}
+
+        private String backend;
+        private String pci_address;
+        private String renderer_features;
+        private boolean renderer_use_egl = true;
+        private boolean renderer_use_gles = true;
+        private boolean renderer_use_glx = false;
+        private boolean renderer_use_surfaceless = true;
+        private boolean renderer_use_vulkan = false;
+        private String[] context_types;
+
+        private GpuConfig toConfig() {
+            return new GpuConfig.Builder()
+                    .setBackend(backend)
+                    .setPciAddress(pci_address)
+                    .setRendererFeatures(renderer_features)
+                    .setRendererUseEgl(renderer_use_egl)
+                    .setRendererUseGles(renderer_use_gles)
+                    .setRendererUseGlx(renderer_use_glx)
+                    .setRendererUseSurfaceless(renderer_use_surfaceless)
+                    .setRendererUseVulkan(renderer_use_vulkan)
+                    .setContextTypes(context_types)
+                    .build();
+        }
+    }
+}
diff --git a/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/Logger.java b/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/Logger.java
new file mode 100644
index 0000000..e1cb285
--- /dev/null
+++ b/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/Logger.java
@@ -0,0 +1,87 @@
+/*
+ * 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.system.virtualmachine.VirtualMachine;
+import android.system.virtualmachine.VirtualMachineConfig;
+import android.system.virtualmachine.VirtualMachineException;
+import android.util.Log;
+
+import libcore.io.Streams;
+
+import java.io.BufferedOutputStream;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.util.concurrent.ExecutorService;
+
+/**
+ * Forwards VM's console output to a file on the Android side, and VM's log output to Android logd.
+ */
+class Logger {
+    private Logger() {}
+
+    static void setup(VirtualMachine vm, Path path, ExecutorService executor) {
+        if (vm.getConfig().getDebugLevel() != VirtualMachineConfig.DEBUG_LEVEL_FULL) {
+            return;
+        }
+
+        try {
+            InputStream console = vm.getConsoleOutput();
+            OutputStream file = Files.newOutputStream(path, StandardOpenOption.CREATE);
+            executor.submit(() -> Streams.copy(console, new LineBufferedOutputStream(file)));
+
+            InputStream log = vm.getLogOutput();
+            executor.submit(() -> writeToLogd(log, vm.getName()));
+        } catch (VirtualMachineException | IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private static boolean writeToLogd(InputStream input, String vmName) throws IOException {
+        BufferedReader reader = new BufferedReader(new InputStreamReader(input));
+        String line;
+        while ((line = reader.readLine()) != null && !Thread.interrupted()) {
+            Log.d(vmName, line);
+        }
+        // TODO: find out why javac complains when the return type of this method is void. It
+        // (incorrectly?) thinks that IOException should be caught inside the lambda.
+        return true;
+    }
+
+    private static class LineBufferedOutputStream extends BufferedOutputStream {
+        LineBufferedOutputStream(OutputStream out) {
+            super(out);
+        }
+
+        @Override
+        public void write(byte[] buf, int off, int len) throws IOException {
+            super.write(buf, off, len);
+            for (int i = 0; i < len; ++i) {
+                if (buf[off + i] == '\n') {
+                    flush();
+                    break;
+                }
+            }
+        }
+    }
+}
diff --git a/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/Runner.java b/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/Runner.java
new file mode 100644
index 0000000..9b97fee
--- /dev/null
+++ b/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/Runner.java
@@ -0,0 +1,114 @@
+/*
+ * 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 = Runner.class.getSimpleName();
+    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 {
+        // context may already be the app context, but calling this again is not harmful.
+        // See b/359439878 on why vmm should be obtained from the app context.
+        Context appContext = context.getApplicationContext();
+        VirtualMachineManager vmm = appContext.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);
+        }
+    }
+}
diff --git a/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/VmLauncherService.java b/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/VmLauncherService.java
new file mode 100644
index 0000000..5e78f99
--- /dev/null
+++ b/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/VmLauncherService.java
@@ -0,0 +1,174 @@
+/*
+ * 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.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.Service;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.ParcelFileDescriptor;
+import android.os.ResultReceiver;
+import android.system.virtualmachine.VirtualMachine;
+import android.system.virtualmachine.VirtualMachineConfig;
+import android.system.virtualmachine.VirtualMachineException;
+import android.util.Log;
+
+import java.io.BufferedReader;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.file.Path;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+public class VmLauncherService extends Service {
+    private static final String TAG = "VmLauncherService";
+    // TODO: this path should be from outside of this service
+    private static final String VM_CONFIG_PATH = "/data/local/tmp/vm_config.json";
+
+    private static final int RESULT_START = 0;
+    private static final int RESULT_STOP = 1;
+    private static final int RESULT_ERROR = 2;
+    private static final int RESULT_IPADDR = 3;
+    private static final String KEY_VM_IP_ADDR = "ip_addr";
+
+    private ExecutorService mExecutorService;
+    private VirtualMachine mVirtualMachine;
+    private ResultReceiver mResultReceiver;
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        return null;
+    }
+
+    private void startForeground() {
+        NotificationManager notificationManager = getSystemService(NotificationManager.class);
+        NotificationChannel notificationChannel =
+                new NotificationChannel(TAG, TAG, NotificationManager.IMPORTANCE_LOW);
+        notificationManager.createNotificationChannel(notificationChannel);
+        startForeground(
+                this.hashCode(),
+                new Notification.Builder(this, TAG)
+                        .setChannelId(TAG)
+                        .setSmallIcon(android.R.drawable.ic_dialog_info)
+                        .setContentText("A VM " + mVirtualMachine.getName() + " is running")
+                        .build());
+    }
+
+    @Override
+    public int onStartCommand(Intent intent, int flags, int startId) {
+        if (isVmRunning()) {
+            Log.d(TAG, "there is already the running VM instance");
+            return START_NOT_STICKY;
+        }
+        mExecutorService = Executors.newCachedThreadPool();
+
+        ConfigJson json = ConfigJson.from(VM_CONFIG_PATH);
+        VirtualMachineConfig config = json.toConfig(this);
+
+        Runner runner;
+        try {
+            runner = Runner.create(this, config);
+        } catch (VirtualMachineException e) {
+            Log.e(TAG, "cannot create runner", e);
+            stopSelf();
+            return START_NOT_STICKY;
+        }
+        mVirtualMachine = runner.getVm();
+        mResultReceiver =
+                intent.getParcelableExtra(Intent.EXTRA_RESULT_RECEIVER, ResultReceiver.class);
+
+        runner.getExitStatus()
+                .thenAcceptAsync(
+                        success -> {
+                            if (mResultReceiver != null) {
+                                mResultReceiver.send(success ? RESULT_STOP : RESULT_ERROR, null);
+                            }
+                            if (!success) {
+                                stopSelf();
+                            }
+                        });
+        Path logPath = getFileStreamPath(mVirtualMachine.getName() + ".log").toPath();
+        Logger.setup(mVirtualMachine, logPath, mExecutorService);
+
+        startForeground();
+
+        mResultReceiver.send(RESULT_START, null);
+        if (config.getCustomImageConfig().useNetwork()) {
+            Handler handler = new Handler(Looper.getMainLooper());
+            gatherIpAddrFromVm(handler);
+        }
+        return START_NOT_STICKY;
+    }
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+        if (isVmRunning()) {
+            try {
+                mVirtualMachine.stop();
+                stopForeground(STOP_FOREGROUND_REMOVE);
+            } catch (VirtualMachineException e) {
+                Log.e(TAG, "failed to stop a VM instance", e);
+            }
+            mExecutorService.shutdownNow();
+            mExecutorService = null;
+            mVirtualMachine = null;
+        }
+    }
+
+    private boolean isVmRunning() {
+        return mVirtualMachine != null
+                && mVirtualMachine.getStatus() == VirtualMachine.STATUS_RUNNING;
+    }
+
+    // TODO(b/359523803): Use AVF API to get ip addr when it exists
+    private void gatherIpAddrFromVm(Handler handler) {
+        handler.postDelayed(
+                () -> {
+                    if (!isVmRunning()) {
+                        Log.d(TAG, "A virtual machine instance isn't running");
+                        return;
+                    }
+                    int INTERNAL_VSOCK_SERVER_PORT = 1024;
+                    try (ParcelFileDescriptor pfd =
+                            mVirtualMachine.connectVsock(INTERNAL_VSOCK_SERVER_PORT)) {
+                        try (BufferedReader input =
+                                new BufferedReader(
+                                        new InputStreamReader(
+                                                new FileInputStream(pfd.getFileDescriptor())))) {
+                            String vmIpAddr = input.readLine().strip();
+                            Bundle b = new Bundle();
+                            b.putString(KEY_VM_IP_ADDR, vmIpAddr);
+                            mResultReceiver.send(RESULT_IPADDR, b);
+                            return;
+                        } catch (IOException e) {
+                            Log.e(TAG, e.toString());
+                        }
+                    } catch (Exception e) {
+                        Log.e(TAG, e.toString());
+                    }
+                    gatherIpAddrFromVm(handler);
+                },
+                1000);
+    }
+}