Merge "Add more strict device assignment test" into main
diff --git a/java/framework/src/android/system/virtualmachine/VirtualMachine.java b/java/framework/src/android/system/virtualmachine/VirtualMachine.java
index a5c8062..f9ca2f2 100644
--- a/java/framework/src/android/system/virtualmachine/VirtualMachine.java
+++ b/java/framework/src/android/system/virtualmachine/VirtualMachine.java
@@ -71,6 +71,7 @@
 import android.system.virtualizationservice.MemoryTrimLevel;
 import android.system.virtualizationservice.PartitionType;
 import android.system.virtualizationservice.VirtualMachineAppConfig;
+import android.system.virtualizationservice.VirtualMachineRawConfig;
 import android.system.virtualizationservice.VirtualMachineState;
 import android.util.JsonReader;
 import android.util.Log;
@@ -810,6 +811,58 @@
         }
     }
 
+    private android.system.virtualizationservice.VirtualMachineConfig
+            createVirtualMachineConfigForRawFrom(VirtualMachineConfig vmConfig)
+                    throws IllegalStateException {
+        VirtualMachineRawConfig rawConfig = vmConfig.toVsRawConfig();
+        return android.system.virtualizationservice.VirtualMachineConfig.rawConfig(rawConfig);
+    }
+
+    private android.system.virtualizationservice.VirtualMachineConfig
+            createVirtualMachineConfigForAppFrom(
+                    VirtualMachineConfig vmConfig, IVirtualizationService service)
+                    throws RemoteException, IOException, VirtualMachineException {
+        VirtualMachineAppConfig appConfig = vmConfig.toVsConfig(mContext.getPackageManager());
+        appConfig.instanceImage = ParcelFileDescriptor.open(mInstanceFilePath, MODE_READ_WRITE);
+        appConfig.name = mName;
+        if (mInstanceIdPath != null) {
+            appConfig.instanceId = Files.readAllBytes(mInstanceIdPath.toPath());
+        } else {
+            // FEATURE_LLPVM_CHANGES is disabled, instance_id is not used.
+            appConfig.instanceId = new byte[64];
+        }
+        if (mEncryptedStoreFilePath != null) {
+            appConfig.encryptedStorageImage =
+                    ParcelFileDescriptor.open(mEncryptedStoreFilePath, MODE_READ_WRITE);
+        }
+
+        if (!vmConfig.getExtraApks().isEmpty()) {
+            // Extra APKs were specified directly, rather than via config file.
+            // We've already populated the file names for the extra APKs and IDSigs
+            // (via setupExtraApks). But we also need to open the APK files and add
+            // fds for them to the payload config.
+            // This isn't needed when the extra APKs are specified in a config file -
+            // then
+            // Virtualization Manager opens them itself.
+            List<ParcelFileDescriptor> extraApkFiles = new ArrayList<>(mExtraApks.size());
+            for (ExtraApkSpec extraApk : mExtraApks) {
+                try {
+                    extraApkFiles.add(ParcelFileDescriptor.open(extraApk.apk, MODE_READ_ONLY));
+                } catch (FileNotFoundException e) {
+                    throw new VirtualMachineException("Failed to open extra APK", e);
+                }
+            }
+            appConfig.payload.getPayloadConfig().extraApks = extraApkFiles;
+        }
+
+        try {
+            createIdSigsAndUpdateConfig(service, appConfig);
+        } catch (FileNotFoundException e) {
+            throw new VirtualMachineException("Failed to generate APK signature", e);
+        }
+        return android.system.virtualizationservice.VirtualMachineConfig.appConfig(appConfig);
+    }
+
     /**
      * Runs this virtual machine. The returning of this method however doesn't mean that the VM has
      * actually started running or the OS has booted there. Such events can be notified by
@@ -850,50 +903,10 @@
                 }
 
                 VirtualMachineConfig vmConfig = getConfig();
-                VirtualMachineAppConfig appConfig =
-                        vmConfig.toVsConfig(mContext.getPackageManager());
-                appConfig.instanceImage =
-                        ParcelFileDescriptor.open(mInstanceFilePath, MODE_READ_WRITE);
-                appConfig.name = mName;
-                if (mInstanceIdPath != null) {
-                    appConfig.instanceId = Files.readAllBytes(mInstanceIdPath.toPath());
-                } else {
-                    // FEATURE_LLPVM_CHANGES is disabled, instance_id is not used.
-                    appConfig.instanceId = new byte[64];
-                }
-                if (mEncryptedStoreFilePath != null) {
-                    appConfig.encryptedStorageImage =
-                            ParcelFileDescriptor.open(mEncryptedStoreFilePath, MODE_READ_WRITE);
-                }
-
-                if (!vmConfig.getExtraApks().isEmpty()) {
-                    // Extra APKs were specified directly, rather than via config file.
-                    // We've already populated the file names for the extra APKs and IDSigs
-                    // (via setupExtraApks). But we also need to open the APK files and add
-                    // fds for them to the payload config.
-                    // This isn't needed when the extra APKs are specified in a config file - then
-                    // Virtualization Manager opens them itself.
-                    List<ParcelFileDescriptor> extraApkFiles = new ArrayList<>(mExtraApks.size());
-                    for (ExtraApkSpec extraApk : mExtraApks) {
-                        try {
-                            extraApkFiles.add(
-                                    ParcelFileDescriptor.open(extraApk.apk, MODE_READ_ONLY));
-                        } catch (FileNotFoundException e) {
-                            throw new VirtualMachineException("Failed to open extra APK", e);
-                        }
-                    }
-                    appConfig.payload.getPayloadConfig().extraApks = extraApkFiles;
-                }
-
-                try {
-                    createIdSigsAndUpdateConfig(service, appConfig);
-                } catch (FileNotFoundException e) {
-                    throw new VirtualMachineException("Failed to generate APK signature", e);
-                }
-
                 android.system.virtualizationservice.VirtualMachineConfig vmConfigParcel =
-                        android.system.virtualizationservice.VirtualMachineConfig.appConfig(
-                                appConfig);
+                        vmConfig.getRawConfigPath() != null
+                                ? createVirtualMachineConfigForRawFrom(vmConfig)
+                                : createVirtualMachineConfigForAppFrom(vmConfig, service);
 
                 mVirtualMachine =
                         service.createVm(
diff --git a/java/framework/src/android/system/virtualmachine/VirtualMachineConfig.java b/java/framework/src/android/system/virtualmachine/VirtualMachineConfig.java
index 054d73c..99c3d05 100644
--- a/java/framework/src/android/system/virtualmachine/VirtualMachineConfig.java
+++ b/java/framework/src/android/system/virtualmachine/VirtualMachineConfig.java
@@ -18,6 +18,7 @@
 
 import static android.os.ParcelFileDescriptor.AutoCloseInputStream;
 import static android.os.ParcelFileDescriptor.MODE_READ_ONLY;
+import static android.os.ParcelFileDescriptor.MODE_READ_WRITE;
 
 import static java.util.Objects.requireNonNull;
 
@@ -37,12 +38,19 @@
 import android.os.ParcelFileDescriptor;
 import android.os.PersistableBundle;
 import android.sysprop.HypervisorProperties;
+import android.system.virtualizationservice.DiskImage;
+import android.system.virtualizationservice.Partition;
 import android.system.virtualizationservice.VirtualMachineAppConfig;
 import android.system.virtualizationservice.VirtualMachinePayloadConfig;
+import android.system.virtualizationservice.VirtualMachineRawConfig;
 import android.util.Log;
 
 import com.android.system.virtualmachine.flags.Flags;
 
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
@@ -52,6 +60,8 @@
 import java.io.OutputStream;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -78,6 +88,7 @@
     private static final String KEY_PACKAGENAME = "packageName";
     private static final String KEY_APKPATH = "apkPath";
     private static final String KEY_PAYLOADCONFIGPATH = "payloadConfigPath";
+    private static final String KEY_RAWCONFIGPATH = "rawConfigPath";
     private static final String KEY_PAYLOADBINARYNAME = "payloadBinaryPath";
     private static final String KEY_DEBUGLEVEL = "debugLevel";
     private static final String KEY_PROTECTED_VM = "protectedVm";
@@ -173,6 +184,9 @@
     /** Name of the payload binary file within the APK that will be executed within the VM. */
     @Nullable private final String mPayloadBinaryName;
 
+    /** Path within the raw config file to launch the VM. */
+    @Nullable private final String mRawConfigPath;
+
     /** The size of storage in bytes. 0 indicates that encryptedStorage is not required */
     private final long mEncryptedStorageBytes;
 
@@ -210,6 +224,7 @@
             List<String> extraApks,
             @Nullable String payloadConfigPath,
             @Nullable String payloadBinaryName,
+            @Nullable String rawConfigPath,
             @DebugLevel int debugLevel,
             boolean protectedVm,
             long memoryBytes,
@@ -229,6 +244,7 @@
                                 Arrays.asList(extraApks.toArray(new String[0])));
         mPayloadConfigPath = payloadConfigPath;
         mPayloadBinaryName = payloadBinaryName;
+        mRawConfigPath = rawConfigPath;
         mDebugLevel = debugLevel;
         mProtectedVm = protectedVm;
         mMemoryBytes = memoryBytes;
@@ -290,10 +306,14 @@
         }
 
         String payloadConfigPath = b.getString(KEY_PAYLOADCONFIGPATH);
-        if (payloadConfigPath == null) {
-            builder.setPayloadBinaryName(b.getString(KEY_PAYLOADBINARYNAME));
-        } else {
+        String payloadBinaryName = b.getString(KEY_PAYLOADBINARYNAME);
+        String rawConfigPath = b.getString(KEY_RAWCONFIGPATH);
+        if (rawConfigPath != null) {
+            builder.setRawConfigPath(rawConfigPath);
+        } else if (payloadConfigPath != null) {
             builder.setPayloadConfigPath(payloadConfigPath);
+        } else {
+            builder.setPayloadBinaryName(payloadBinaryName);
         }
 
         @DebugLevel int debugLevel = b.getInt(KEY_DEBUGLEVEL);
@@ -352,6 +372,7 @@
         }
         b.putString(KEY_PAYLOADCONFIGPATH, mPayloadConfigPath);
         b.putString(KEY_PAYLOADBINARYNAME, mPayloadBinaryName);
+        b.putString(KEY_RAWCONFIGPATH, mRawConfigPath);
         b.putInt(KEY_DEBUGLEVEL, mDebugLevel);
         b.putBoolean(KEY_PROTECTED_VM, mProtectedVm);
         b.putInt(KEY_CPU_TOPOLOGY, mCpuTopology);
@@ -412,6 +433,16 @@
     }
 
     /**
+     * Returns the path within the raw config file to launch the VM.
+     *
+     * @hide
+     */
+    @Nullable
+    public String getRawConfigPath() {
+        return mRawConfigPath;
+    }
+
+    /**
      * Returns the name of the payload binary file, in the {@code lib/<ABI>} directory of the APK,
      * that will be executed within the VM.
      *
@@ -554,6 +585,99 @@
                 && Objects.equals(this.mExtraApks, other.mExtraApks);
     }
 
+    // Needs to sync with packages/modules/Virtualization/libs/vmconfig/src/lib.rs
+    VirtualMachineRawConfig toVsRawConfig() throws IllegalStateException {
+        VirtualMachineRawConfig config = new VirtualMachineRawConfig();
+
+        try {
+            String rawJson = new String(Files.readAllBytes(Path.of(mRawConfigPath)));
+            JSONObject json = new JSONObject(rawJson);
+            config.name = json.optString("name", "");
+            config.instanceId = new byte[64];
+            if (json.has("kernel")) {
+                config.kernel =
+                        ParcelFileDescriptor.open(
+                                new File(json.getString("kernel")), MODE_READ_ONLY);
+            }
+            if (json.has("initrd")) {
+                config.initrd =
+                        ParcelFileDescriptor.open(
+                                new File(json.getString("initrd")), MODE_READ_ONLY);
+            }
+            if (json.has("params")) {
+                config.params = json.getString("params");
+            }
+            if (json.has("bootloader")) {
+                config.bootloader =
+                        ParcelFileDescriptor.open(
+                                new File(json.getString("bootloader")), MODE_READ_ONLY);
+            }
+            if (json.has("disks")) {
+                JSONArray diskArr = json.getJSONArray("disks");
+                config.disks = new DiskImage[diskArr.length()];
+                for (int i = 0; i < diskArr.length(); i++) {
+                    config.disks[i] = new DiskImage();
+                    JSONObject item = diskArr.getJSONObject(i);
+                    config.disks[i].writable = item.optBoolean("writable", false);
+                    if (item.has("image")) {
+                        config.disks[i].image =
+                                ParcelFileDescriptor.open(
+                                        new File(item.getString("image")),
+                                        config.disks[i].writable
+                                                ? MODE_READ_WRITE
+                                                : MODE_READ_ONLY);
+                    }
+                    if (item.has("partition")) {
+                        JSONArray partitionArr = json.getJSONArray("partition");
+                        config.disks[i].partitions = new Partition[partitionArr.length()];
+                        for (int j = 0; j < partitionArr.length(); j++) {
+                            config.disks[i].partitions[j] = new Partition();
+                            JSONObject partitionItem = partitionArr.getJSONObject(j);
+                            config.disks[i].partitions[j].writable =
+                                    partitionItem.optBoolean("writable", false);
+                            config.disks[i].partitions[j].label = partitionItem.getString("label");
+                            config.disks[i].partitions[j].image =
+                                    ParcelFileDescriptor.open(
+                                            new File(partitionItem.getString("image")),
+                                            config.disks[i].partitions[j].writable
+                                                    ? MODE_READ_WRITE
+                                                    : MODE_READ_ONLY);
+                        }
+                    } else {
+                        config.disks[i].partitions = new Partition[0];
+                    }
+                }
+            } else {
+                config.disks = new DiskImage[0];
+            }
+            // The value which is set by setProtectedVm is used.
+            if (json.has("protected")) {
+                Log.d(TAG, "'protected' field is ignored, the value from setProtectedVm is used");
+            }
+            config.protectedVm = this.mProtectedVm;
+            config.memoryMib = json.optInt("memory_mib", 0);
+            if (json.optString("cpu_topology", "one_cpu").equals("match_host")) {
+                config.cpuTopology = CPU_TOPOLOGY_MATCH_HOST;
+            } else {
+                config.cpuTopology = CPU_TOPOLOGY_ONE_CPU;
+            }
+            config.platformVersion = json.getString("platform_version");
+            if (json.has("devices")) {
+                JSONArray arr = json.getJSONArray("devices");
+                config.devices = new String[arr.length()];
+                for (int i = 0; i < arr.length(); i++) {
+                    config.devices[i] = arr.getString(i);
+                }
+            } else {
+                config.devices = new String[0];
+            }
+
+        } catch (JSONException | IOException e) {
+            throw new IllegalStateException("malformed input", e);
+        }
+        return config;
+    }
+
     /**
      * Converts this config object into the parcelable type used when creating a VM via the
      * virtualization service. Notice that the files are not passed as paths, but as file
@@ -680,6 +804,7 @@
         @Nullable private String mApkPath;
         private final List<String> mExtraApks = new ArrayList<>();
         @Nullable private String mPayloadConfigPath;
+        @Nullable private String mRawConfigPath;
         @Nullable private String mPayloadBinaryName;
         @DebugLevel private int mDebugLevel = DEBUG_LEVEL_NONE;
         private boolean mProtectedVm;
@@ -729,8 +854,13 @@
                 // This should never happen, unless we're deserializing a bad config
                 throw new IllegalStateException("apkPath or packageName must be specified");
             }
-
-            if (mPayloadBinaryName == null) {
+            if (mRawConfigPath != null) {
+                if (mPayloadBinaryName != null || mPayloadConfigPath != null) {
+                    throw new IllegalStateException(
+                            "setRawConfigPath and (setPayloadBinaryName or setPayloadConfigPath)"
+                                    + " may not both be called");
+                }
+            } else if (mPayloadBinaryName == null) {
                 if (mPayloadConfigPath == null) {
                     throw new IllegalStateException("setPayloadBinaryName must be called");
                 }
@@ -763,6 +893,7 @@
                     mExtraApks,
                     mPayloadConfigPath,
                     mPayloadBinaryName,
+                    mRawConfigPath,
                     mDebugLevel,
                     mProtectedVm,
                     mMemoryBytes,
@@ -824,6 +955,29 @@
         }
 
         /**
+         * Sets the path within the raw config file to launch the VM. The file is a JSON file; see
+         * packages/modules/Virtualization/libs/vmconfig/src/lib.rs for the format.
+         *
+         * @hide
+         */
+        @RequiresPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION)
+        @NonNull
+        public Builder setRawConfigPath(@NonNull String rawConfigPath) {
+            // TODO: This method will be removed when the builder support more structured methods
+            // for that like below.
+            //    builder
+            //      .setLinuxKernelConfig(new LinuxKernelConfig()
+            //        .setKernelPath(...)
+            //        .setInitRamdiskPath(...)
+            //        .setKernelCommandLine(...))
+            //      .setDiskConfig(new DiskConfig()
+            //        .addDisk(...)
+            //        .addDisk(...))
+            mRawConfigPath = requireNonNull(rawConfigPath, "rawConfigPath must not be null");
+            return this;
+        }
+
+        /**
          * Sets the name of the payload binary file that will be executed within the VM, e.g.
          * "payload.so". The file must reside in the {@code lib/<ABI>} directory of the APK.
          *
diff --git a/libs/bssl/src/lib.rs b/libs/bssl/src/lib.rs
index ad51b61..686abf9 100644
--- a/libs/bssl/src/lib.rs
+++ b/libs/bssl/src/lib.rs
@@ -15,6 +15,7 @@
 //! Safe wrappers around the BoringSSL API.
 
 #![cfg_attr(not(feature = "std"), no_std)]
+#![warn(clippy::or_fun_call)]
 
 extern crate alloc;
 
diff --git a/vmlauncher_app/Android.bp b/vmlauncher_app/Android.bp
new file mode 100644
index 0000000..cd40448
--- /dev/null
+++ b/vmlauncher_app/Android.bp
@@ -0,0 +1,20 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_app {
+    name: "VmLauncherApp",
+    srcs: ["java/**/*.java"],
+    resource_dirs: ["res"],
+    static_libs: [
+        "androidx-constraintlayout_constraintlayout",
+        "androidx.appcompat_appcompat",
+        "com.google.android.material_material",
+    ],
+    libs: [
+        "framework-virtualization.impl",
+        "framework-annotations-lib",
+    ],
+    platform_apis: true,
+    privileged: true,
+}
diff --git a/vmlauncher_app/AndroidManifest.xml b/vmlauncher_app/AndroidManifest.xml
new file mode 100644
index 0000000..de9d094
--- /dev/null
+++ b/vmlauncher_app/AndroidManifest.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.virtualization.vmlauncher" >
+
+    <uses-permission android:name="android.permission.MANAGE_VIRTUAL_MACHINE" />
+    <uses-permission android:name="android.permission.USE_CUSTOM_VIRTUAL_MACHINE" />
+    <uses-feature android:name="android.software.virtualization_framework" android:required="true" />
+    <application
+        android:label="VmLauncherApp">
+        <activity android:name=".MainActivity" android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+    </application>
+
+</manifest>
diff --git a/vmlauncher_app/README.md b/vmlauncher_app/README.md
new file mode 100644
index 0000000..9175e57
--- /dev/null
+++ b/vmlauncher_app/README.md
@@ -0,0 +1,16 @@
+# VM launcher app
+
+## Building & Installing
+
+Add `VmLauncherApp` into `PRODUCT_PACKAGES` and then `m`
+
+You can also explicitly grant or revoke the permission, e.g.
+```
+adb shell pm grant com.android.virtualization.vmlauncher android.permission.USE_CUSTOM_VIRTUAL_MACHINE
+adb shell pm grant com.android.virtualization.vmlauncher android.permission.MANAGE_VIRTUAL_MACHINE
+```
+
+## Running
+
+Copy vm config json file to /data/local/tmp/vm_config.json.
+And then, run the app, check log meesage.
diff --git a/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java b/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java
new file mode 100644
index 0000000..90d7fcc
--- /dev/null
+++ b/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java
@@ -0,0 +1,153 @@
+/*
+ * 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.Activity;
+import android.os.Bundle;
+import android.util.Log;
+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 java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+public class MainActivity extends Activity {
+    private static final String TAG = "VmLauncherApp";
+    private static final String VM_NAME = "my_custom_vm";
+    private static final boolean DEBUG = true;
+    private final ExecutorService mExecutorService = Executors.newFixedThreadPool(4);
+    private VirtualMachine mVirtualMachine;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        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) {
+                        Log.e(TAG, "payload start");
+                    }
+
+                    @Override
+                    public void onPayloadReady(VirtualMachine vm) {
+                        // This check doesn't 100% prevent race condition or UI hang.
+                        // However, it's fine for demo.
+                        if (mService.isShutdown()) {
+                            return;
+                        }
+                        Log.d(TAG, "(Payload is ready. Testing VM service...)");
+                    }
+
+                    @Override
+                    public void onPayloadFinished(VirtualMachine vm, int exitCode) {
+                        // This check doesn't 100% prevent race condition, but is fine for demo.
+                        if (!mService.isShutdown()) {
+                            Log.d(
+                                    TAG,
+                                    String.format("(Payload finished. exit code: %d)", exitCode));
+                        }
+                    }
+
+                    @Override
+                    public void onError(VirtualMachine vm, int errorCode, String message) {
+                        Log.d(
+                                TAG,
+                                String.format(
+                                        "(Error occurred. code: %d, message: %s)",
+                                        errorCode, message));
+                    }
+
+                    @Override
+                    public void onStopped(VirtualMachine vm, int reason) {
+                        Log.e(TAG, "vm stop");
+                    }
+                };
+
+        try {
+            VirtualMachineConfig.Builder builder =
+                    new VirtualMachineConfig.Builder(getApplication());
+            builder.setRawConfigPath("/data/local/tmp/vm_config.json");
+            builder.setProtectedVm(false);
+            if (DEBUG) {
+                builder.setDebugLevel(VirtualMachineConfig.DEBUG_LEVEL_FULL);
+                builder.setVmOutputCaptured(true);
+            }
+            VirtualMachineConfig config = builder.build();
+            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" + e);
+            }
+
+            Log.d(TAG, "vm start");
+            mVirtualMachine.run();
+            mVirtualMachine.setCallback(Executors.newSingleThreadExecutor(), callback);
+            if (DEBUG) {
+                InputStream console = mVirtualMachine.getConsoleOutput();
+                InputStream log = mVirtualMachine.getLogOutput();
+                mExecutorService.execute(new Reader("console", console));
+                mExecutorService.execute(new Reader("log", log));
+            }
+        } catch (VirtualMachineException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /** Reads data from an input stream and posts it to the output data */
+    static class Reader implements Runnable {
+        private final String mName;
+        private final InputStream mStream;
+
+        Reader(String name, InputStream stream) {
+            mName = name;
+            mStream = stream;
+        }
+
+        @Override
+        public void run() {
+            try {
+                BufferedReader reader = new BufferedReader(new InputStreamReader(mStream));
+                String line;
+                while ((line = reader.readLine()) != null && !Thread.interrupted()) {
+                    Log.d(TAG, mName + ": " + line);
+                }
+            } catch (IOException e) {
+                Log.e(TAG, "Exception while posting " + mName + " output: " + e.getMessage());
+            }
+        }
+    }
+}
diff --git a/vmlauncher_app/res/layout/activity_main.xml b/vmlauncher_app/res/layout/activity_main.xml
new file mode 100644
index 0000000..5cbda78
--- /dev/null
+++ b/vmlauncher_app/res/layout/activity_main.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:scrollbars="horizontal|vertical"
+    android:textAlignment="textStart"
+    tools:context=".MainActivity">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="vertical">
+
+    </LinearLayout>
+
+</androidx.constraintlayout.widget.ConstraintLayout>