Merge "VmLauncherApp: Small fix for writing \0 in the end of clipboard text" into main
diff --git a/ferrochrome_app/custom_vm_setup.sh b/ferrochrome_app/custom_vm_setup.sh
index f007f6a..a5480ff 100644
--- a/ferrochrome_app/custom_vm_setup.sh
+++ b/ferrochrome_app/custom_vm_setup.sh
@@ -1,14 +1,31 @@
 #!/system/bin/sh
 
-function copy_files() {
-  cp -u /sdcard/vm_config.json /data/local/tmp
-  cp -u /data/media/10/vm_config.json /data/local/tmp
-  cp -u /sdcard/chromiumos_test_image.bin /data/local/tmp
-  cp -u /data/media/10/chromiumos_test_image.bin /data/local/tmp
-  chmod 666 /data/local/tmp/vm_config.json
-  chmod 666 /data/local/tmp/chromiumos_test_image.bin
+function round_up() {
+  num=$1
+  div=$2
+  echo $((( (( ${num} / ${div} ) + 1) * ${div} )))
 }
+
+function install() {
+  user=$(cmd user get-main-user)
+  src_dir=/data/media/${user}/ferrochrome/
+  dst_dir=/data/local/tmp/
+
+  cat $(find ${src_dir} -name "images.tar.gz*" | sort) | tar xz -C ${dst_dir}
+  cp -u ${src_dir}vm_config.json ${dst_dir}
+  chmod 666 ${dst_dir}*
+
+  # increase the size of state.img to the multiple of 4096
+  num_blocks=$(du -b -K ${dst_dir}state.img | cut -f 1)
+  required_num_blocks=$(round_up ${num_blocks} 4)
+  additional_blocks=$((( ${required_num_blocks} - ${num_blocks} )))
+  dd if=/dev/zero bs=512 count=${additional_blocks} >> ${dst_dir}state.img
+
+  rm ${src_dir}images.tar.gz*
+  rm ${src_dir}vm_config.json
+}
+
 setprop debug.custom_vm_setup.done false
-copy_files
+install
 setprop debug.custom_vm_setup.start false
 setprop debug.custom_vm_setup.done true
diff --git a/ferrochrome_app/java/com/android/virtualization/ferrochrome/FerrochromeActivity.java b/ferrochrome_app/java/com/android/virtualization/ferrochrome/FerrochromeActivity.java
index 7c18537..58005aa 100644
--- a/ferrochrome_app/java/com/android/virtualization/ferrochrome/FerrochromeActivity.java
+++ b/ferrochrome_app/java/com/android/virtualization/ferrochrome/FerrochromeActivity.java
@@ -28,15 +28,11 @@
 import android.view.WindowManager;
 import android.widget.TextView;
 
-import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
-import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
-import org.apache.commons.compress.compressors.xz.XZCompressorInputStream;
-
-import java.io.File;
+import java.io.BufferedReader;
+import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStream;
-import java.net.URL;
-import java.nio.charset.StandardCharsets;
+import java.io.InputStreamReader;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.StandardCopyOption;
@@ -45,19 +41,17 @@
 import java.util.concurrent.Executors;
 
 public class FerrochromeActivity extends Activity {
-    ExecutorService executorService = Executors.newSingleThreadExecutor();
-    private static final String TAG = "FerrochromeActivity";
+    private static final String TAG = FerrochromeActivity.class.getName();
     private static final String ACTION_VM_LAUNCHER = "android.virtualization.VM_LAUNCHER";
-    private static final String FERROCHROME_VERSION = "R128-15926.0.0";
-    private static final String EXTERNAL_STORAGE_DIR =
-            Environment.getExternalStorageDirectory().getPath() + File.separator;
-    private static final Path IMAGE_PATH =
-            Path.of(EXTERNAL_STORAGE_DIR + "chromiumos_test_image.bin");
-    private static final Path IMAGE_VERSION_INFO =
-            Path.of(EXTERNAL_STORAGE_DIR + "ferrochrome_image_version");
-    private static final Path VM_CONFIG_PATH = Path.of(EXTERNAL_STORAGE_DIR + "vm_config.json");
+
+    private static final Path DEST_DIR =
+            Path.of(Environment.getExternalStorageDirectory().getPath(), "ferrochrome");
+    private static final Path VERSION_FILE = Path.of(DEST_DIR.toString(), "version");
+
     private static final int REQUEST_CODE_VMLAUNCHER = 1;
 
+    ExecutorService executorService = Executors.newSingleThreadExecutor();
+
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
@@ -80,35 +74,7 @@
 
         executorService.execute(
                 () -> {
-                    if (Files.notExists(IMAGE_PATH)
-                            || !FERROCHROME_VERSION.equals(getVersionInfo())) {
-                        updateStatus("Starting first-time setup.");
-                        updateStatus(
-                                "Downloading Ferrochrome image. This can take about 5 to 10"
-                                        + " minutes, depending on your network speed.");
-                        if (download(FERROCHROME_VERSION)) {
-                            updateStatus("Done.");
-                        } else {
-                            updateStatus(
-                                    "Download failed. Check the internet connection and retry.");
-                            return;
-                        }
-                    } else {
-                        updateStatus("Ferrochrome is already downloaded.");
-                    }
-                    updateStatus("Updating VM config.");
-                    copyVmConfigJson();
-                    updateStatus("Updating VM images. This may take a few minutes.");
-                    SystemProperties.set("debug.custom_vm_setup.start", "true");
-                    while (!SystemProperties.getBoolean("debug.custom_vm_setup.done", false)) {
-                        // Wait for custom_vm_setup
-                        try {
-                            Thread.sleep(1000);
-                        } catch (Exception e) {
-                            Log.d(TAG, e.toString());
-                        }
-                    }
-                    updateStatus("Done.");
+                    updateImageIfNeeded();
                     updateStatus("Starting Ferrochrome...");
                     runOnUiThread(() -> startActivityForResult(intent, REQUEST_CODE_VMLAUNCHER));
                 });
@@ -121,63 +87,86 @@
         }
     }
 
+    private void updateImageIfNeeded() {
+        if (!isUpdateNeeded()) {
+            Log.d(TAG, "No update needed.");
+            return;
+        }
+
+        updateStatus("Copying images...");
+        try {
+            if (Files.notExists(DEST_DIR)) {
+                Files.createDirectory(DEST_DIR);
+            }
+            for (String file : getAssets().list("ferrochrome")) {
+                updateStatus(file);
+                Path dst = Path.of(DEST_DIR.toString(), file);
+                updateFile(getAssets().open("ferrochrome/" + file), dst);
+            }
+        } catch (IOException e) {
+            Log.e(TAG, "Error while updating image: " + e);
+            updateStatus("Failed.");
+            return;
+        }
+        updateStatus("Done.");
+
+        updateStatus("Extracting images...");
+        SystemProperties.set("debug.custom_vm_setup.start", "true");
+        while (!SystemProperties.getBoolean("debug.custom_vm_setup.done", false)) {
+            try {
+                Thread.sleep(1000);
+            } catch (Exception e) {
+                Log.e(TAG, "Error while extracting image: " + e);
+                updateStatus("Failed.");
+                return;
+            }
+        }
+        updateStatus("Done.");
+    }
+
+    private boolean isUpdateNeeded() {
+        Path[] pathsToCheck = {DEST_DIR, VERSION_FILE};
+        for (Path p : pathsToCheck) {
+            if (Files.notExists(p)) {
+                Log.d(TAG, p.toString() + " does not exist.");
+                return true;
+            }
+        }
+
+        try {
+            String installedVer = readLine(new FileInputStream(VERSION_FILE.toFile()));
+            String updatedVer = readLine(getAssets().open("ferrochrome/version"));
+            if (installedVer.equals(updatedVer)) {
+                return false;
+            }
+            Log.d(TAG, "Version mismatch. Installed: " + installedVer + "  Updated: " + updatedVer);
+        } catch (IOException e) {
+            Log.e(TAG, "Error while checking version: " + e);
+        }
+        return true;
+    }
+
+    private static String readLine(InputStream input) throws IOException {
+        try (BufferedReader reader = new BufferedReader(new InputStreamReader(input))) {
+            return reader.readLine();
+        } catch (IOException e) {
+            throw e;
+        }
+    }
+
+    private static void updateFile(InputStream input, Path path) throws IOException {
+        try {
+            Files.copy(input, path, StandardCopyOption.REPLACE_EXISTING);
+        } finally {
+            input.close();
+        }
+    }
+
     private void updateStatus(String line) {
-        Log.d(TAG, line);
         runOnUiThread(
                 () -> {
                     TextView statusView = findViewById(R.id.status_txt_view);
                     statusView.append(line + "\n");
                 });
     }
-
-    private void copyVmConfigJson() {
-        try (InputStream is = getResources().openRawResource(R.raw.vm_config)) {
-            Files.copy(is, VM_CONFIG_PATH, StandardCopyOption.REPLACE_EXISTING);
-        } catch (IOException e) {
-            updateStatus(e.toString());
-        }
-    }
-
-    private String getVersionInfo() {
-        try {
-            return new String(Files.readAllBytes(IMAGE_VERSION_INFO), StandardCharsets.UTF_8);
-        } catch (IOException e) {
-            return null;
-        }
-    }
-
-    private boolean updateVersionInfo(String version) {
-        try {
-            Files.write(IMAGE_VERSION_INFO, version.getBytes(StandardCharsets.UTF_8));
-        } catch (IOException e) {
-            Log.d(TAG, e.toString());
-        }
-        return true;
-    }
-
-    private boolean download(String version) {
-        String urlString =
-                "https://storage.googleapis.com/chromiumos-image-archive/ferrochrome-public/"
-                        + version
-                        + "/chromiumos_test_image.tar.xz";
-        try (InputStream is = (new URL(urlString)).openStream();
-                XZCompressorInputStream xz = new XZCompressorInputStream(is);
-                TarArchiveInputStream tar = new TarArchiveInputStream(xz)) {
-            TarArchiveEntry entry;
-            while ((entry = tar.getNextTarEntry()) != null) {
-                if (!entry.getName().contains("chromiumos_test_image.bin")) {
-                    continue;
-                }
-                updateStatus("copy " + entry.getName() + " start");
-                Files.copy(tar, IMAGE_PATH, StandardCopyOption.REPLACE_EXISTING);
-                updateStatus("copy " + entry.getName() + " done");
-                updateVersionInfo(version);
-                break;
-            }
-        } catch (Exception e) {
-            updateStatus(e.toString());
-            return false;
-        }
-        return true;
-    }
 }
diff --git a/ferrochrome_app/repack.sh b/ferrochrome_app/repack.sh
new file mode 100755
index 0000000..d47b529
--- /dev/null
+++ b/ferrochrome_app/repack.sh
@@ -0,0 +1,53 @@
+#!/bin/bash
+# Repacks chromiumos_*.bin into the assets of FerrochromeApp
+
+usage() {
+	echo "Usage: $0 CHROME_OS_DISK_IMAGE"
+	exit 1
+}
+
+if [ "$#" -ne 1 ]; then
+	usage
+fi
+
+disk=$1
+
+loop=$(sudo losetup --show -f -P ${disk})
+kern=$(sudo fdisk -x ${loop} | grep KERN-A | awk "{print\$1}")
+root=$(sudo fdisk -x ${loop} | grep ROOT-A | awk "{print\$1}")
+efi=$(sudo fdisk -x ${loop} | grep EFI-SYSTEM | awk "{print\$1}")
+state=$(sudo fdisk -x ${loop} | grep STATE | awk "{print\$1}")
+root_guid=$(sudo fdisk -x ${loop} | grep ROOT-A | awk "{print\$6}")
+
+tempdir=$(mktemp -d)
+pushd ${tempdir} > /dev/null
+echo Extracting partition images...
+sudo cp --sparse=always ${kern} kernel.img
+sudo cp --sparse=always ${root} root.img
+sudo cp --sparse=always ${efi} efi.img
+sudo cp --sparse=always ${state} state.img
+sudo chmod 777 *.img
+
+echo Archiving. This can take long...
+tar czvS -f images.tar.gz *.img
+
+echo Calculating hash...
+hash=$(sha1sum images.tar.gz | cut -d' ' -f 1)
+
+echo Splitting...
+split -b 100M -d images.tar.gz images.tar.gz.part
+
+popd > /dev/null
+asset_dir=$(dirname $0)/assets/ferrochrome
+echo Updating ${asset_dir}...
+vm_config_template=$(dirname $0)/vm_config.json.template
+mkdir -p ${asset_dir}
+rm ${asset_dir}/images.tar.gz.part*
+mv ${tempdir}/images.tar.gz.part* ${asset_dir}
+sed -E s/GUID/${root_guid}/ ${vm_config_template} > ${asset_dir}/vm_config.json
+echo ${hash} > ${asset_dir}/version
+
+echo Cleanup...
+sudo losetup -d ${loop}
+rm -rf ${tempdir}
+echo Done.
diff --git a/ferrochrome_app/res/raw/vm_config.json b/ferrochrome_app/res/raw/vm_config.json
deleted file mode 100644
index d79400c..0000000
--- a/ferrochrome_app/res/raw/vm_config.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
-    "name": "cros",
-    "disks": [
-        {
-            "image": "/data/local/tmp/chromiumos_test_image.bin",
-            "partitions": [],
-            "writable": true
-        }
-    ],
-    "params": "root=/dev/vda3 rootwait noinitrd ro enforcing=0 cros_debug cros_secure",
-    "protected": false,
-    "cpu_topology": "match_host",
-    "platform_version": "~1.0",
-    "memory_mib": 8096,
-    "gpu": {
-        "backend": "virglrenderer",
-        "context_types": ["virgl2"]
-    },
-    "console_input_device": "ttyS0"
-}
\ No newline at end of file
diff --git a/ferrochrome_app/vm_config.json.template b/ferrochrome_app/vm_config.json.template
new file mode 100644
index 0000000..cb968ec
--- /dev/null
+++ b/ferrochrome_app/vm_config.json.template
@@ -0,0 +1,37 @@
+{
+    "name": "cros",
+    "disks": [
+        {
+            "writable": true,
+            "partitions": [
+                {
+                    "label": "STATE",
+                    "path": "/data/local/tmp/state.img",
+                    "writable": true
+                },
+                {
+                    "label": "KERN-A",
+                    "path": "/data/local/tmp/kernel.img"
+                },
+                {
+                    "label": "ROOT-A",
+                    "path": "/data/local/tmp/root.img",
+                    "guid": "GUID"
+                },
+                {
+                    "label": "EFI-SYSTEM",
+                    "path": "/data/local/tmp/efi.img"
+                }
+            ]
+        }
+    ],
+    "protected": false,
+    "cpu_topology": "match_host",
+    "platform_version": "~1.0",
+    "memory_mib": 8096,
+    "gpu": {
+        "backend": "virglrenderer",
+        "context_types": ["virgl2"]
+    },
+    "console_input_device": "ttyS0"
+}
diff --git a/java/framework/src/android/system/virtualmachine/VirtualMachine.java b/java/framework/src/android/system/virtualmachine/VirtualMachine.java
index 1d64ee4..23269d9 100644
--- a/java/framework/src/android/system/virtualmachine/VirtualMachine.java
+++ b/java/framework/src/android/system/virtualmachine/VirtualMachine.java
@@ -310,7 +310,7 @@
     /** Running instance of virtmgr that hosts VirtualizationService for this VM. */
     @NonNull private final VirtualizationService mVirtualizationService;
 
-    @NonNull private final MemoryManagementCallbacks mMemoryManagementCallbacks;
+    private final MemoryManagementCallbacks mMemoryManagementCallbacks;
 
     @NonNull private final Context mContext;
 
@@ -441,7 +441,6 @@
         mInstanceFilePath = new File(thisVmDir, INSTANCE_IMAGE_FILE);
         mIdsigFilePath = new File(thisVmDir, IDSIG_FILE);
         mExtraApks = setupExtraApks(context, config, thisVmDir);
-        mMemoryManagementCallbacks = new MemoryManagementCallbacks();
         mContext = context;
         mEncryptedStoreFilePath =
                 (config.isEncryptedStorageEnabled())
@@ -451,6 +450,14 @@
         mVmOutputCaptured = config.isVmOutputCaptured();
         mVmConsoleInputSupported = config.isVmConsoleInputSupported();
         mConnectVmConsole = config.isConnectVmConsole();
+
+        VirtualMachineCustomImageConfig customImageConfig;
+        customImageConfig = config.getCustomImageConfig();
+        if (customImageConfig == null || customImageConfig.useAutoMemoryBalloon()) {
+            mMemoryManagementCallbacks = new MemoryManagementCallbacks();
+        } else {
+            mMemoryManagementCallbacks = null;
+        }
     }
 
     /**
@@ -820,7 +827,9 @@
      */
     @GuardedBy("mLock")
     private void dropVm() {
-        mContext.unregisterComponentCallbacks(mMemoryManagementCallbacks);
+        if (mMemoryManagementCallbacks != null) {
+            mContext.unregisterComponentCallbacks(mMemoryManagementCallbacks);
+        }
         mVirtualMachine = null;
     }
 
@@ -903,11 +912,11 @@
             if (vmConfig.getCustomImageConfig().useTouch()) {
                 ParcelFileDescriptor[] pfds = ParcelFileDescriptor.createSocketPair();
                 mTouchSock = pfds[0];
-                InputDevice.SingleTouch t = new InputDevice.SingleTouch();
+                InputDevice.MultiTouch t = new InputDevice.MultiTouch();
                 t.width = rawConfig.displayConfig.width;
                 t.height = rawConfig.displayConfig.height;
                 t.pfd = pfds[1];
-                inputDevices.add(InputDevice.singleTouch(t));
+                inputDevices.add(InputDevice.multiTouch(t));
             }
             if (vmConfig.getCustomImageConfig().useKeyboard()) {
                 ParcelFileDescriptor[] pfds = ParcelFileDescriptor.createSocketPair();
@@ -1061,7 +1070,7 @@
     }
 
     /** @hide */
-    public boolean sendSingleTouchEvent(MotionEvent event) {
+    public boolean sendMultiTouchEvent(MotionEvent event) {
         if (mTouchSock == null) {
             Log.d(TAG, "mTouchSock == null");
             return false;
@@ -1074,17 +1083,56 @@
         short ABS_X = 0x00;
         short ABS_Y = 0x01;
         short SYN_REPORT = 0x00;
+        short ABS_MT_SLOT = 0x2f;
+        short ABS_MT_POSITION_X = 0x35;
+        short ABS_MT_POSITION_Y = 0x36;
+        short ABS_MT_TRACKING_ID = 0x39;
 
-        int x = (int) event.getX();
-        int y = (int) event.getY();
-        boolean down = event.getAction() != MotionEvent.ACTION_UP;
+        switch (event.getActionMasked()) {
+            case MotionEvent.ACTION_MOVE:
+                List<InputEvent> events =
+                        new ArrayList<>(
+                                event.getPointerCount() * 6 /*InputEvent per a pointer*/
+                                        + 1 /*SYN*/);
+                for (int actionIdx = 0; actionIdx < event.getPointerCount(); actionIdx++) {
+                    int pointerId = event.getPointerId(actionIdx);
+                    int x = (int) event.getRawX(actionIdx);
+                    int y = (int) event.getRawY(actionIdx);
+                    events.add(new InputEvent(EV_ABS, ABS_MT_SLOT, pointerId));
+                    events.add(new InputEvent(EV_ABS, ABS_MT_TRACKING_ID, pointerId));
+                    events.add(new InputEvent(EV_ABS, ABS_MT_POSITION_X, x));
+                    events.add(new InputEvent(EV_ABS, ABS_MT_POSITION_Y, y));
+                    events.add(new InputEvent(EV_ABS, ABS_X, x));
+                    events.add(new InputEvent(EV_ABS, ABS_Y, y));
+                }
+                events.add(new InputEvent(EV_SYN, SYN_REPORT, 0));
+                return writeEventsToSock(mTouchSock, events);
+            case MotionEvent.ACTION_DOWN:
+            case MotionEvent.ACTION_POINTER_DOWN:
+            case MotionEvent.ACTION_UP:
+            case MotionEvent.ACTION_POINTER_UP:
+                break;
+            default:
+                return false;
+        }
 
+        boolean down =
+                event.getActionMasked() == MotionEvent.ACTION_DOWN
+                        || event.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN;
+        int actionIdx = event.getActionIndex();
+        int pointerId = event.getPointerId(actionIdx);
+        int x = (int) event.getRawX(actionIdx);
+        int y = (int) event.getRawY(actionIdx);
         return writeEventsToSock(
                 mTouchSock,
                 Arrays.asList(
+                        new InputEvent(EV_KEY, BTN_TOUCH, down ? 1 : 0),
+                        new InputEvent(EV_ABS, ABS_MT_SLOT, pointerId),
+                        new InputEvent(EV_ABS, ABS_MT_TRACKING_ID, down ? pointerId : -1),
+                        new InputEvent(EV_ABS, ABS_MT_POSITION_X, x),
+                        new InputEvent(EV_ABS, ABS_MT_POSITION_Y, y),
                         new InputEvent(EV_ABS, ABS_X, x),
                         new InputEvent(EV_ABS, ABS_Y, y),
-                        new InputEvent(EV_KEY, BTN_TOUCH, down ? 1 : 0),
                         new InputEvent(EV_SYN, SYN_REPORT, 0)));
     }
 
@@ -1254,6 +1302,46 @@
                         new InputEvent(EV_SYN, SYN_REPORT, 0)));
     }
 
+    /** @hide */
+    public long getMemoryBalloon() {
+        long bytes = 0;
+
+        if (mMemoryManagementCallbacks != null) {
+            Log.d(TAG, "Auto balloon enabled in getMemoryBalloon");
+            return bytes;
+        }
+
+        synchronized (mLock) {
+            try {
+                if (mVirtualMachine != null) {
+                    bytes = mVirtualMachine.getMemoryBalloon();
+                }
+            } catch (RemoteException e) {
+                Log.w(TAG, "Cannot getMemoryBalloon", e);
+            }
+        }
+
+        return bytes;
+    }
+
+    /** @hide */
+    public void setMemoryBalloon(long bytes) {
+        if (mMemoryManagementCallbacks != null) {
+            Log.d(TAG, "Auto balloon enabled in setMemoryBalloon");
+            return;
+        }
+
+        synchronized (mLock) {
+            try {
+                if (mVirtualMachine != null) {
+                    mVirtualMachine.setMemoryBalloon(bytes);
+                }
+            } catch (RemoteException e) {
+                Log.w(TAG, "Cannot setMemoryBalloon", e);
+            }
+        }
+    }
+
     private boolean writeEventsToSock(ParcelFileDescriptor sock, List<InputEvent> evtList) {
         ByteBuffer byteBuffer =
                 ByteBuffer.allocate(8 /* (type: u16 + code: u16 + value: i32) */ * evtList.size());
@@ -1405,7 +1493,9 @@
                 mVirtualMachine =
                         service.createVm(vmConfigParcel, consoleOutFd, consoleInFd, mLogWriter);
                 mVirtualMachine.registerCallback(new CallbackTranslator(service));
-                mContext.registerComponentCallbacks(mMemoryManagementCallbacks);
+                if (mMemoryManagementCallbacks != null) {
+                    mContext.registerComponentCallbacks(mMemoryManagementCallbacks);
+                }
                 if (mConnectVmConsole) {
                     mVirtualMachine.setHostConsoleName(getHostConsoleName());
                 }
diff --git a/java/framework/src/android/system/virtualmachine/VirtualMachineCustomImageConfig.java b/java/framework/src/android/system/virtualmachine/VirtualMachineCustomImageConfig.java
index 3a1c784..2da83a0 100644
--- a/java/framework/src/android/system/virtualmachine/VirtualMachineCustomImageConfig.java
+++ b/java/framework/src/android/system/virtualmachine/VirtualMachineCustomImageConfig.java
@@ -45,6 +45,7 @@
     private static final String KEY_GPU = "gpu";
     private static final String KEY_AUDIO_CONFIG = "audio_config";
     private static final String KEY_TRACKPAD = "trackpad";
+    private static final String KEY_AUTO_MEMORY_BALLOON = "auto_memory_balloon";
 
     @Nullable private final String name;
     @Nullable private final String kernelPath;
@@ -61,6 +62,7 @@
     private final boolean network;
     @Nullable private final GpuConfig gpuConfig;
     private final boolean trackpad;
+    private final boolean autoMemoryBalloon;
 
     @Nullable
     public Disk[] getDisks() {
@@ -112,6 +114,10 @@
         return mouse;
     }
 
+    public boolean useAutoMemoryBalloon() {
+        return autoMemoryBalloon;
+    }
+
     public boolean useNetwork() {
         return network;
     }
@@ -132,7 +138,8 @@
             boolean network,
             GpuConfig gpuConfig,
             AudioConfig audioConfig,
-            boolean trackpad) {
+            boolean trackpad,
+            boolean autoMemoryBalloon) {
         this.name = name;
         this.kernelPath = kernelPath;
         this.initrdPath = initrdPath;
@@ -148,6 +155,7 @@
         this.gpuConfig = gpuConfig;
         this.audioConfig = audioConfig;
         this.trackpad = trackpad;
+        this.autoMemoryBalloon = autoMemoryBalloon;
     }
 
     static VirtualMachineCustomImageConfig from(PersistableBundle customImageConfigBundle) {
@@ -199,6 +207,7 @@
                 customImageConfigBundle.getPersistableBundle(KEY_AUDIO_CONFIG);
         builder.setAudioConfig(AudioConfig.from(audioConfigPb));
         builder.useTrackpad(customImageConfigBundle.getBoolean(KEY_TRACKPAD));
+        builder.useAutoMemoryBalloon(customImageConfigBundle.getBoolean(KEY_AUTO_MEMORY_BALLOON));
         return builder.build();
     }
 
@@ -258,6 +267,7 @@
                 KEY_AUDIO_CONFIG,
                 Optional.ofNullable(audioConfig).map(ac -> ac.toPersistableBundle()).orElse(null));
         pb.putBoolean(KEY_TRACKPAD, trackpad);
+        pb.putBoolean(KEY_AUTO_MEMORY_BALLOON, autoMemoryBalloon);
         return pb;
     }
 
@@ -352,6 +362,7 @@
         private boolean network;
         private GpuConfig gpuConfig;
         private boolean trackpad;
+        private boolean autoMemoryBalloon = true;
 
         /** @hide */
         public Builder() {}
@@ -435,6 +446,12 @@
         }
 
         /** @hide */
+        public Builder useAutoMemoryBalloon(boolean autoMemoryBalloon) {
+            this.autoMemoryBalloon = autoMemoryBalloon;
+            return this;
+        }
+
+        /** @hide */
         public Builder useNetwork(boolean network) {
             this.network = network;
             return this;
@@ -463,7 +480,8 @@
                     network,
                     gpuConfig,
                     audioConfig,
-                    trackpad);
+                    trackpad,
+                    autoMemoryBalloon);
         }
     }
 
diff --git a/virtualizationmanager/src/aidl.rs b/virtualizationmanager/src/aidl.rs
index 2d83963..d62a4b4 100644
--- a/virtualizationmanager/src/aidl.rs
+++ b/virtualizationmanager/src/aidl.rs
@@ -813,6 +813,12 @@
             width: u32::try_from(trackpad.width)?,
             name: if !trackpad.name.is_empty() { Some(trackpad.name.clone()) } else { None },
         },
+        InputDevice::MultiTouch(multi_touch) => InputDeviceOption::MultiTouch {
+            file: clone_file(multi_touch.pfd.as_ref().ok_or(anyhow!("pfd should have value"))?)?,
+            height: u32::try_from(multi_touch.height)?,
+            width: u32::try_from(multi_touch.width)?,
+            name: if !multi_touch.name.is_empty() { Some(multi_touch.name.clone()) } else { None },
+        },
     })
 }
 /// Given the configuration for a disk image, assembles the `DiskFile` to pass to crosvm.
diff --git a/virtualizationmanager/src/crosvm.rs b/virtualizationmanager/src/crosvm.rs
index 852d7de..78dd9a2 100644
--- a/virtualizationmanager/src/crosvm.rs
+++ b/virtualizationmanager/src/crosvm.rs
@@ -222,6 +222,7 @@
     Mouse(File),
     Switches(File),
     MultiTouchTrackpad { file: File, width: u32, height: u32, name: Option<String> },
+    MultiTouch { file: File, width: u32, height: u32, name: Option<String> },
 }
 
 type VfioDevice = Strong<dyn IBoundDevice>;
@@ -1153,6 +1154,13 @@
                     height,
                     name.as_ref().map_or("".into(), |n| format!(",name={}", n))
                 ),
+                InputDeviceOption::MultiTouch { file, width, height, name } => format!(
+                    "multi-touch[path={},width={},height={}{}]",
+                    add_preserved_fd(&mut preserved_fds, file),
+                    width,
+                    height,
+                    name.as_ref().map_or("".into(), |n| format!(",name={}", n))
+                ),
             });
         }
     }
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/InputDevice.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/InputDevice.aidl
index e998d02..bb06fff 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/InputDevice.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/InputDevice.aidl
@@ -51,10 +51,20 @@
         int height = 1080;
         @utf8InCpp String name = "";
     }
+
+    parcelable MultiTouch {
+        ParcelFileDescriptor pfd;
+        // Default values come from https://crosvm.dev/book/devices/input.html#multi-touch
+        int width = 1280;
+        int height = 1080;
+        @utf8InCpp String name = "";
+    }
+
     SingleTouch singleTouch;
     EvDev evDev;
     Keyboard keyboard;
     Mouse mouse;
     Switches switches;
     Trackpad trackpad;
+    MultiTouch multiTouch;
 }
diff --git a/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java b/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java
index bbf4e4f..8bb0fd4 100644
--- a/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java
+++ b/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java
@@ -352,7 +352,7 @@
                     if (mVirtualMachine == null) {
                         return false;
                     }
-                    return mVirtualMachine.sendSingleTouchEvent(event);
+                    return mVirtualMachine.sendMultiTouchEvent(event);
                 });
         surfaceView.requestUnbufferedDispatch(InputDevice.SOURCE_ANY);
         surfaceView.setOnCapturedPointerListener(