Merge "Add more details about Service VM to its doc" into main
diff --git a/apex/Android.bp b/apex/Android.bp
index 0eb8b9e..99b2dee 100644
--- a/apex/Android.bp
+++ b/apex/Android.bp
@@ -192,6 +192,9 @@
             androidManifest: "AndroidManifest.xml",
         },
         release_avf_enable_network: {
+            prebuilts: [
+                "com.android.virt.vmnic.rc",
+            ],
             arch: {
                 arm64: {
                     binaries: ["vmnic"],
@@ -256,14 +259,21 @@
     name: "com.android.virt.init.rc",
     src: ":virtualizationservice_rc_combined",
     filename: "virtualizationservice.rc",
-    installable: false,
+    no_full_install: true,
 }
 
 prebuilt_etc {
     name: "com.android.virt.vfio_handler.rc",
     src: "vfio_handler.rc",
     filename: "vfio_handler.rc",
-    installable: false,
+    no_full_install: true,
+}
+
+prebuilt_etc {
+    name: "com.android.virt.vmnic.rc",
+    src: "vmnic.rc",
+    filename: "vmnic.rc",
+    no_full_install: true,
 }
 
 // Virt apex needs a custom signer for its payload
diff --git a/virtualizationservice/vmnic/vmnic.rc b/apex/vmnic.rc
similarity index 100%
rename from virtualizationservice/vmnic/vmnic.rc
rename to apex/vmnic.rc
diff --git a/docs/custom_vm.md b/docs/custom_vm.md
index 7b30835..0825f06 100644
--- a/docs/custom_vm.md
+++ b/docs/custom_vm.md
@@ -25,6 +25,35 @@
 The `vm` command also has other subcommands for debugging; run
 `/apex/com.android.virt/bin/vm help` for details.
 
+### Running Debian with u-boot
+1. Prepare u-boot binary from `u-boot_crosvm_aarch64` in https://ci.android.com/builds/branches/aosp_u-boot-mainline/grid
+or build it by https://source.android.com/docs/devices/cuttlefish/bootloader-dev#develop-bootloader
+2. Prepare Debian image from https://cloud.debian.org/images/cloud/ (We tested nocloud image)
+3. Copy `u-boot.bin`, Debian image file(like `debian-12-nocloud-arm64.raw`) and `vm_config.json` to `/data/local/tmp`
+```shell
+cat > vm_config.json <<EOF
+{
+    "name": "debian",
+    "bootloader": "/data/local/tmp/u-boot.bin",
+    "disks": [
+        {
+            "image": "/data/local/tmp/debian-12-nocloud-arm64.raw",
+            "partitions": [],
+            "writable": true
+        }
+    ],
+    "protected": false,
+    "cpu_topology": "match_host",
+    "platform_version": "~1.0",
+    "memory_mib" : 8096
+}
+EOF
+adb push `u-boot.bin` /data/local/tmp
+adb push `debian-12-nocloud-arm64.raw` /data/local/tmp
+adb push vm_config.json /data/local/tmp/vm_config.json
+```
+4. Launch VmLauncherApp(the detail will be explain below)
+
 ## Graphical VMs
 
 To run OSes with graphics support, follow the instruction below.
diff --git a/java/framework/src/android/system/virtualmachine/VirtualMachine.java b/java/framework/src/android/system/virtualmachine/VirtualMachine.java
index 1076219..2f6e306 100644
--- a/java/framework/src/android/system/virtualmachine/VirtualMachine.java
+++ b/java/framework/src/android/system/virtualmachine/VirtualMachine.java
@@ -164,6 +164,7 @@
 
     private ParcelFileDescriptor mTouchSock;
     private ParcelFileDescriptor mKeySock;
+    private ParcelFileDescriptor mMouseSock;
 
     /**
      * Status of a virtual machine
@@ -881,6 +882,13 @@
                 k.pfd = pfds[1];
                 inputDevices.add(InputDevice.keyboard(k));
             }
+            if (vmConfig.getCustomImageConfig().useMouse()) {
+                ParcelFileDescriptor[] pfds = ParcelFileDescriptor.createSocketPair();
+                mMouseSock = pfds[0];
+                InputDevice.Mouse m = new InputDevice.Mouse();
+                m.pfd = pfds[1];
+                inputDevices.add(InputDevice.mouse(m));
+            }
         }
         rawConfig.inputDevices = inputDevices.toArray(new InputDevice[0]);
 
@@ -909,6 +917,94 @@
     }
 
     /** @hide */
+    public boolean sendMouseEvent(MotionEvent event) {
+        if (mMouseSock == null) {
+            Log.d(TAG, "mMouseSock == null");
+            return false;
+        }
+        // from include/uapi/linux/input-event-codes.h in the kernel.
+        short EV_SYN = 0x00;
+        short EV_REL = 0x02;
+        short EV_KEY = 0x01;
+        short REL_X = 0x00;
+        short REL_Y = 0x01;
+        short SYN_REPORT = 0x00;
+        switch (event.getAction()) {
+            case MotionEvent.ACTION_MOVE:
+                int x = (int) event.getX();
+                int y = (int) event.getY();
+                return writeEventsToSock(
+                        mMouseSock,
+                        Arrays.asList(
+                                new InputEvent(EV_REL, REL_X, x),
+                                new InputEvent(EV_REL, REL_Y, y),
+                                new InputEvent(EV_SYN, SYN_REPORT, 0)));
+            case MotionEvent.ACTION_BUTTON_PRESS:
+            case MotionEvent.ACTION_BUTTON_RELEASE:
+                short BTN_LEFT = 0x110;
+                short BTN_RIGHT = 0x111;
+                short BTN_MIDDLE = 0x112;
+                short keyCode;
+                switch (event.getActionButton()) {
+                    case MotionEvent.BUTTON_PRIMARY:
+                        keyCode = BTN_LEFT;
+                        break;
+                    case MotionEvent.BUTTON_SECONDARY:
+                        keyCode = BTN_RIGHT;
+                        break;
+                    case MotionEvent.BUTTON_TERTIARY:
+                        keyCode = BTN_MIDDLE;
+                        break;
+                    default:
+                        Log.d(TAG, event.toString());
+                        return false;
+                }
+                return writeEventsToSock(
+                        mMouseSock,
+                        Arrays.asList(
+                                new InputEvent(
+                                        EV_KEY,
+                                        keyCode,
+                                        event.getAction() == MotionEvent.ACTION_BUTTON_PRESS
+                                                ? 1
+                                                : 0),
+                                new InputEvent(EV_SYN, SYN_REPORT, 0)));
+            case MotionEvent.ACTION_SCROLL:
+                short REL_HWHEEL = 0x06;
+                short REL_WHEEL = 0x08;
+                int scrollX = (int) event.getAxisValue(MotionEvent.AXIS_HSCROLL);
+                int scrollY = (int) event.getAxisValue(MotionEvent.AXIS_VSCROLL);
+                boolean status = true;
+                if (scrollX != 0) {
+                    status &=
+                            writeEventsToSock(
+                                    mMouseSock,
+                                    Arrays.asList(
+                                            new InputEvent(EV_REL, REL_HWHEEL, scrollX),
+                                            new InputEvent(EV_SYN, SYN_REPORT, 0)));
+                } else if (scrollY != 0) {
+                    status &=
+                            writeEventsToSock(
+                                    mMouseSock,
+                                    Arrays.asList(
+                                            new InputEvent(EV_REL, REL_WHEEL, scrollY),
+                                            new InputEvent(EV_SYN, SYN_REPORT, 0)));
+                } else {
+                    Log.d(TAG, event.toString());
+                    return false;
+                }
+                return status;
+            case MotionEvent.ACTION_UP:
+            case MotionEvent.ACTION_DOWN:
+                // Ignored because it's handled by ACTION_BUTTON_PRESS and ACTION_BUTTON_RELEASE
+                return true;
+            default:
+                Log.d(TAG, event.toString());
+                return false;
+        }
+    }
+
+    /** @hide */
     public boolean sendSingleTouchEvent(MotionEvent event) {
         if (mTouchSock == null) {
             Log.d(TAG, "mTouchSock == null");
diff --git a/java/framework/src/android/system/virtualmachine/VirtualMachineCustomImageConfig.java b/java/framework/src/android/system/virtualmachine/VirtualMachineCustomImageConfig.java
index 8ec9d2c..2fcad20 100644
--- a/java/framework/src/android/system/virtualmachine/VirtualMachineCustomImageConfig.java
+++ b/java/framework/src/android/system/virtualmachine/VirtualMachineCustomImageConfig.java
@@ -35,6 +35,7 @@
     private static final String KEY_DISPLAY_CONFIG = "display_config";
     private static final String KEY_TOUCH = "touch";
     private static final String KEY_KEYBOARD = "keyboard";
+    private static final String KEY_MOUSE = "mouse";
 
     @Nullable private final String name;
     @Nullable private final String kernelPath;
@@ -45,6 +46,7 @@
     @Nullable private final DisplayConfig displayConfig;
     private final boolean touch;
     private final boolean keyboard;
+    private final boolean mouse;
 
     @Nullable
     public Disk[] getDisks() {
@@ -84,6 +86,10 @@
         return keyboard;
     }
 
+    public boolean useMouse() {
+        return mouse;
+    }
+
     /** @hide */
     public VirtualMachineCustomImageConfig(
             String name,
@@ -94,7 +100,8 @@
             Disk[] disks,
             DisplayConfig displayConfig,
             boolean touch,
-            boolean keyboard) {
+            boolean keyboard,
+            boolean mouse) {
         this.name = name;
         this.kernelPath = kernelPath;
         this.initrdPath = initrdPath;
@@ -104,6 +111,7 @@
         this.displayConfig = displayConfig;
         this.touch = touch;
         this.keyboard = keyboard;
+        this.mouse = mouse;
     }
 
     static VirtualMachineCustomImageConfig from(PersistableBundle customImageConfigBundle) {
@@ -133,6 +141,7 @@
         builder.setDisplayConfig(DisplayConfig.from(displayConfigPb));
         builder.useTouch(customImageConfigBundle.getBoolean(KEY_TOUCH));
         builder.useKeyboard(customImageConfigBundle.getBoolean(KEY_KEYBOARD));
+        builder.useMouse(customImageConfigBundle.getBoolean(KEY_MOUSE));
         return builder.build();
     }
 
@@ -163,6 +172,7 @@
                         .orElse(null));
         pb.putBoolean(KEY_TOUCH, touch);
         pb.putBoolean(KEY_KEYBOARD, keyboard);
+        pb.putBoolean(KEY_MOUSE, mouse);
         return pb;
     }
 
@@ -213,6 +223,7 @@
         private DisplayConfig displayConfig;
         private boolean touch;
         private boolean keyboard;
+        private boolean mouse;
 
         /** @hide */
         public Builder() {}
@@ -272,6 +283,12 @@
         }
 
         /** @hide */
+        public Builder useMouse(boolean mouse) {
+            this.mouse = mouse;
+            return this;
+        }
+
+        /** @hide */
         public VirtualMachineCustomImageConfig build() {
             return new VirtualMachineCustomImageConfig(
                     this.name,
@@ -282,7 +299,8 @@
                     this.disks.toArray(new Disk[0]),
                     displayConfig,
                     touch,
-                    keyboard);
+                    keyboard,
+                    mouse);
         }
     }
 
diff --git a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidCapabilitiesTest.java b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidCapabilitiesTest.java
index 61f4cba..c50e59a 100644
--- a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidCapabilitiesTest.java
+++ b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidCapabilitiesTest.java
@@ -43,7 +43,6 @@
 public class MicrodroidCapabilitiesTest extends MicrodroidDeviceTestBase {
     @Test
     @CddTest(requirements = "9.17/C-1-6")
-    @Ignore("b/326092480")
     public void supportForProtectedOrNonProtectedVms() {
         assumeSupportedDevice();
 
diff --git a/virtualizationmanager/src/aidl.rs b/virtualizationmanager/src/aidl.rs
index a245e11..796ae21 100644
--- a/virtualizationmanager/src/aidl.rs
+++ b/virtualizationmanager/src/aidl.rs
@@ -14,7 +14,7 @@
 
 //! Implementation of the AIDL interface of the VirtualizationService.
 
-use crate::{get_calling_pid, get_calling_uid};
+use crate::{get_calling_pid, get_calling_uid, get_this_pid};
 use crate::atom::{write_vm_booted_stats, write_vm_creation_stats};
 use crate::composite::make_composite_image;
 use crate::crosvm::{CrosvmConfig, DiskFile, DisplayConfig, InputDeviceOption, PayloadState, VmContext, VmInstance, VmState};
@@ -606,6 +606,18 @@
             vec![]
         };
 
+        // Create TAP network interface if the VM supports network.
+        let _tap_fd = if cfg!(network) && config.networkSupported {
+            if *is_protected {
+                return Err(anyhow!("Network feature is not supported for pVM yet"))
+                    .with_log()
+                    .or_binder_exception(ExceptionCode::UNSUPPORTED_OPERATION)?;
+            }
+            Some(GLOBAL_SERVICE.createTapInterface(&get_this_pid().to_string())?)
+        } else {
+            None
+        };
+
         // Actually start the VM.
         let crosvm_config = CrosvmConfig {
             cid,
@@ -757,6 +769,9 @@
         InputDevice::Keyboard(keyboard) => InputDeviceOption::Keyboard(clone_file(
             keyboard.pfd.as_ref().ok_or(anyhow!("pfd should have value"))?,
         )?),
+        InputDevice::Mouse(mouse) => InputDeviceOption::Mouse(clone_file(
+            mouse.pfd.as_ref().ok_or(anyhow!("pfd should have value"))?,
+        )?),
     })
 }
 /// Given the configuration for a disk image, assembles the `DiskFile` to pass to crosvm.
@@ -910,6 +925,7 @@
         }
 
         vm_config.devices.clone_from(&custom_config.devices);
+        vm_config.networkSupported = custom_config.networkSupported;
     }
 
     if config.memoryMib > 0 {
diff --git a/virtualizationmanager/src/crosvm.rs b/virtualizationmanager/src/crosvm.rs
index b426051..d48ef7b 100644
--- a/virtualizationmanager/src/crosvm.rs
+++ b/virtualizationmanager/src/crosvm.rs
@@ -163,6 +163,7 @@
     EvDev(File),
     SingleTouch { file: File, width: u32, height: u32, name: Option<String> },
     Keyboard(File),
+    Mouse(File),
 }
 
 type VfioDevice = Strong<dyn IBoundDevice>;
@@ -996,6 +997,9 @@
                 InputDeviceOption::Keyboard(file) => {
                     format!("keyboard[path={}]", add_preserved_fd(&mut preserved_fds, file))
                 }
+                InputDeviceOption::Mouse(file) => {
+                    format!("mouse[path={}]", add_preserved_fd(&mut preserved_fds, file))
+                }
                 InputDeviceOption::SingleTouch { file, width, height, name } => format!(
                     "single-touch[path={},width={},height={}{}]",
                     add_preserved_fd(&mut preserved_fds, file),
diff --git a/virtualizationmanager/src/main.rs b/virtualizationmanager/src/main.rs
index a31fd0a..4e88507 100644
--- a/virtualizationmanager/src/main.rs
+++ b/virtualizationmanager/src/main.rs
@@ -39,10 +39,16 @@
 const LOG_TAG: &str = "virtmgr";
 
 lazy_static! {
+    static ref PID_CURRENT: Pid = Pid::this();
     static ref PID_PARENT: Pid = Pid::parent();
     static ref UID_CURRENT: Uid = Uid::current();
 }
 
+fn get_this_pid() -> pid_t {
+    // Return the process ID of this process.
+    PID_CURRENT.as_raw()
+}
+
 fn get_calling_pid() -> pid_t {
     // The caller is the parent of this process.
     PID_PARENT.as_raw()
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/InputDevice.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/InputDevice.aidl
index 712d6a9..56c5b6d 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/InputDevice.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/InputDevice.aidl
@@ -34,7 +34,12 @@
     parcelable Keyboard {
         ParcelFileDescriptor pfd;
     }
+    // Mouse input
+    parcelable Mouse {
+        ParcelFileDescriptor pfd;
+    }
     SingleTouch singleTouch;
     EvDev evDev;
     Keyboard keyboard;
+    Mouse mouse;
 }
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineAppConfig.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineAppConfig.aidl
index 9951bfd..a3f4b0f 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineAppConfig.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineAppConfig.aidl
@@ -124,6 +124,9 @@
          * should rarely need to be set false.
          */
         boolean wantUpdatable = true;
+
+        /** Whether the VM should have network feature. */
+        boolean networkSupported;
     }
 
     /** Configuration parameters guarded by android.permission.USE_CUSTOM_VIRTUAL_MACHINE */
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl
index cf9d25a..a5a849a 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl
@@ -85,4 +85,7 @@
 
     /** List of input devices to the VM */
     InputDevice[] inputDevices;
+
+    /** Whether the VM should have network feature. */
+    boolean networkSupported;
 }
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVirtualizationServiceInternal.aidl b/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVirtualizationServiceInternal.aidl
index 84f8734..4e6879d 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVirtualizationServiceInternal.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVirtualizationServiceInternal.aidl
@@ -121,4 +121,11 @@
     void setDisplayService(IBinder ibinder);
     void clearDisplayService();
     IBinder waitDisplayService();
+
+    /**
+     * Create TAP network interface for a VM.
+     * @param suffix of network interface name.
+     * @return file descriptor of the TAP network interface.
+     */
+    ParcelFileDescriptor createTapInterface(String ifaceNameSuffix);
 }
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVmnic.aidl b/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVmnic.aidl
index 3796763..66739da 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVmnic.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVmnic.aidl
@@ -18,8 +18,8 @@
 interface IVmnic {
     /**
      * Create TAP network interface for a VM.
-     * @param CID of VM.
+     * @param suffix of network interface name.
      * @return file descriptor of the TAP network interface.
      */
-    ParcelFileDescriptor createTapInterface(int cid);
+    ParcelFileDescriptor createTapInterface(String ifaceNameSuffix);
 }
diff --git a/virtualizationservice/src/aidl.rs b/virtualizationservice/src/aidl.rs
index 1fa634d..5dbfe22 100644
--- a/virtualizationservice/src/aidl.rs
+++ b/virtualizationservice/src/aidl.rs
@@ -70,6 +70,7 @@
     IVfioHandler::VfioDev::VfioDev,
     IVfioHandler::{BpVfioHandler, IVfioHandler},
     IVirtualizationServiceInternal::IVirtualizationServiceInternal,
+    IVmnic::{BpVmnic, IVmnic},
 };
 use virtualmachineservice::IVirtualMachineService::VM_TOMBSTONES_SERVICE_PORT;
 use vsock::{VsockListener, VsockStream};
@@ -159,6 +160,9 @@
     static ref VFIO_SERVICE: Strong<dyn IVfioHandler> =
         wait_for_interface(<BpVfioHandler as IVfioHandler>::get_descriptor())
             .expect("Could not connect to VfioHandler");
+    static ref NETWORK_SERVICE: Strong<dyn IVmnic> =
+        wait_for_interface(<BpVmnic as IVmnic>::get_descriptor())
+            .expect("Could not connect to Vmnic");
 }
 
 fn is_valid_guest_cid(cid: Cid) -> bool {
@@ -494,6 +498,18 @@
         }
         Ok(())
     }
+
+    fn createTapInterface(&self, iface_name_suffix: &str) -> binder::Result<ParcelFileDescriptor> {
+        check_use_custom_virtual_machine()?;
+        if !cfg!(network) {
+            return Err(Status::new_exception_str(
+                ExceptionCode::UNSUPPORTED_OPERATION,
+                Some("createTapInterface is not supported with the network feature disabled"),
+            ))
+            .with_log();
+        }
+        NETWORK_SERVICE.createTapInterface(iface_name_suffix)
+    }
 }
 
 impl IVirtualizationMaintenance for VirtualizationServiceInternal {
diff --git a/virtualizationservice/vmnic/Android.bp b/virtualizationservice/vmnic/Android.bp
index 4313a82..784c648 100644
--- a/virtualizationservice/vmnic/Android.bp
+++ b/virtualizationservice/vmnic/Android.bp
@@ -17,5 +17,4 @@
         "liblog_rust",
     ],
     apex_available: ["com.android.virt"],
-    init_rc: ["vmnic.rc"],
 }
diff --git a/virtualizationservice/vmnic/src/aidl.rs b/virtualizationservice/vmnic/src/aidl.rs
index 26a0eff..6443258 100644
--- a/virtualizationservice/vmnic/src/aidl.rs
+++ b/virtualizationservice/vmnic/src/aidl.rs
@@ -17,6 +17,7 @@
 use anyhow::anyhow;
 use android_system_virtualizationservice_internal::aidl::android::system::virtualizationservice_internal::IVmnic::IVmnic;
 use binder::{self, ExceptionCode, Interface, IntoBinderResult, ParcelFileDescriptor};
+use log::info;
 
 #[derive(Debug, Default)]
 pub struct Vmnic {}
@@ -30,7 +31,10 @@
 impl Interface for Vmnic {}
 
 impl IVmnic for Vmnic {
-    fn createTapInterface(&self, _cid: i32) -> binder::Result<ParcelFileDescriptor> {
+    fn createTapInterface(&self, iface_name_suffix: &str) -> binder::Result<ParcelFileDescriptor> {
+        let ifname = format!("avf_tap_{iface_name_suffix}");
+        info!("Creating TAP interface {}", ifname);
+
         Err(anyhow!("Creating TAP network interface is not supported yet"))
             .or_binder_exception(ExceptionCode::UNSUPPORTED_OPERATION)
     }
diff --git a/vm/src/main.rs b/vm/src/main.rs
index d6ee3a5..390a60d 100644
--- a/vm/src/main.rs
+++ b/vm/src/main.rs
@@ -60,6 +60,23 @@
     /// https://docs.kernel.org/admin-guide/mm/transhuge.html
     #[arg(short, long)]
     hugepages: bool,
+
+    /// Run VM with network feature.
+    #[cfg(network)]
+    #[arg(short, long)]
+    network_supported: bool,
+}
+
+impl CommonConfig {
+    #[cfg(network)]
+    fn network_supported(&self) -> bool {
+        self.network_supported
+    }
+
+    #[cfg(not(network))]
+    fn network_supported(&self) -> bool {
+        false
+    }
 }
 
 #[derive(Args, Default)]
diff --git a/vm/src/run.rs b/vm/src/run.rs
index 5e797f8..0c9fbb6 100644
--- a/vm/src/run.rs
+++ b/vm/src/run.rs
@@ -159,6 +159,7 @@
                 x.to_str().map(String::from).ok_or(anyhow!("Failed to convert {x:?} to String"))
             })
             .collect::<Result<_, _>>()?,
+        networkSupported: config.common.network_supported(),
         ..Default::default()
     };
 
diff --git a/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java b/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java
index ec0f8e8..10f8bf6 100644
--- a/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java
+++ b/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java
@@ -35,9 +35,11 @@
 import android.system.virtualmachine.VirtualMachineException;
 import android.system.virtualmachine.VirtualMachineManager;
 import android.view.Display;
+import android.view.InputDevice;
 import android.view.SurfaceHolder;
 import android.view.SurfaceView;
 import android.view.KeyEvent;
+import android.view.View;
 import android.view.WindowManager;
 import android.view.WindowInsets;
 import android.view.WindowInsetsController;
@@ -129,6 +131,7 @@
             customImageConfigBuilder.setDisplayConfig(displayConfigBuilder.build());
             customImageConfigBuilder.useTouch(true);
             customImageConfigBuilder.useKeyboard(true);
+            customImageConfigBuilder.useMouse(true);
 
             configBuilder.setCustomImageConfig(customImageConfigBuilder.build());
 
@@ -244,13 +247,22 @@
         }
 
         SurfaceView surfaceView = findViewById(R.id.surface_view);
-        surfaceView.setOnTouchListener(
+        View backgroundTouchView = findViewById(R.id.background_touch_view);
+        backgroundTouchView.setOnTouchListener(
                 (v, event) -> {
                     if (mVirtualMachine == null) {
                         return false;
                     }
                     return mVirtualMachine.sendSingleTouchEvent(event);
                 });
+        surfaceView.requestUnbufferedDispatch(InputDevice.SOURCE_ANY);
+        surfaceView.setOnCapturedPointerListener(
+                (v, event) -> {
+                    if (mVirtualMachine == null) {
+                        return false;
+                    }
+                    return mVirtualMachine.sendMouseEvent(event);
+                });
         surfaceView
                 .getHolder()
                 .addCallback(
@@ -292,6 +304,16 @@
         windowInsetsController.hide(WindowInsets.Type.systemBars());
     }
 
+    @Override
+    public void onWindowFocusChanged(boolean hasFocus) {
+        super.onWindowFocusChanged(hasFocus);
+        if (hasFocus) {
+            SurfaceView surfaceView = findViewById(R.id.surface_view);
+            Log.d(TAG, "requestPointerCapture()");
+            surfaceView.requestPointerCapture();
+        }
+    }
+
     @FunctionalInterface
     public interface RemoteExceptionCheckedFunction<T> {
         void apply(T t) throws RemoteException;
diff --git a/vmlauncher_app/res/layout/activity_main.xml b/vmlauncher_app/res/layout/activity_main.xml
index 5fa2171..e52dfcd 100644
--- a/vmlauncher_app/res/layout/activity_main.xml
+++ b/vmlauncher_app/res/layout/activity_main.xml
@@ -1,14 +1,24 @@
 <?xml version="1.0" encoding="utf-8"?>
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<merge 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"
     tools:context=".MainActivity">
-    <SurfaceView
+  <View
+      android:id="@+id/background_touch_view"
+      android:layout_width="match_parent"
+      android:layout_height="match_parent"
+    />
+  <SurfaceView
         android:id="@+id/surface_view"
-        android:focusable="true"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent" />
+      android:layout_width="match_parent"
+      android:layout_height="match_parent"
+      android:focusable="true"
+      android:focusableInTouchMode="true"
+      android:focusedByDefault="true"
+      android:defaultFocusHighlightEnabled="true">
+    <requestFocus />
+  </SurfaceView>
 
-</LinearLayout>
+</merge>