Merge "Suspend the VM when it's not visible on the screen" into main
diff --git a/docs/custom_vm.md b/docs/custom_vm.md
index 13ee620..6c51795 100644
--- a/docs/custom_vm.md
+++ b/docs/custom_vm.md
@@ -56,7 +56,13 @@
 
 ## Graphical VMs
 
-To run OSes with graphics support, follow the instruction below.
+To run OSes with graphics support, simply
+`packages/modules/Virtualization/tests/ferrochrome/ferrochrome.sh`. It prepares
+and launches the ChromiumOS, which is the only officially supported guest
+payload. We will be adding more OSes in the future.
+
+If you want to do so by yourself (e.g. boot with your build), follow the
+instruction below.
 
 ### Prepare a guest image
 
diff --git a/ferrochrome_app/java/com/android/virtualization/ferrochrome/FerrochromeActivity.java b/ferrochrome_app/java/com/android/virtualization/ferrochrome/FerrochromeActivity.java
index d9e5229..7c18537 100644
--- a/ferrochrome_app/java/com/android/virtualization/ferrochrome/FerrochromeActivity.java
+++ b/ferrochrome_app/java/com/android/virtualization/ferrochrome/FerrochromeActivity.java
@@ -56,6 +56,7 @@
     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 int REQUEST_CODE_VMLAUNCHER = 1;
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
@@ -109,10 +110,17 @@
                     }
                     updateStatus("Done.");
                     updateStatus("Starting Ferrochrome...");
-                    runOnUiThread(() -> startActivity(intent));
+                    runOnUiThread(() -> startActivityForResult(intent, REQUEST_CODE_VMLAUNCHER));
                 });
     }
 
+    @Override
+    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+        if (requestCode == REQUEST_CODE_VMLAUNCHER) {
+            finishAndRemoveTask();
+        }
+    }
+
     private void updateStatus(String line) {
         Log.d(TAG, line);
         runOnUiThread(
diff --git a/pvmfw/platform.dts b/pvmfw/platform.dts
index 2df0768..44834ed 100644
--- a/pvmfw/platform.dts
+++ b/pvmfw/platform.dts
@@ -411,6 +411,7 @@
 		reg = <0x00 0x3000 0x00 0x1000>;
 		clock-frequency = <10>;
 		timeout-sec = <8>;
+		interrupts = <GIC_PPI 0xf PLACEHOLDER>;
 	};
 
 	cpufreq {
diff --git a/pvmfw/src/fdt.rs b/pvmfw/src/fdt.rs
index 84dc14d..939a4ea 100644
--- a/pvmfw/src/fdt.rs
+++ b/pvmfw/src/fdt.rs
@@ -759,6 +759,84 @@
     Ok(SerialInfo { addrs })
 }
 
+#[derive(Default, Debug, PartialEq)]
+struct WdtInfo {
+    addr: u64,
+    size: u64,
+    irq: [u32; WdtInfo::IRQ_CELLS],
+}
+
+impl WdtInfo {
+    const IRQ_CELLS: usize = 3;
+    const IRQ_NR: u32 = 0xf;
+    const ADDR: u64 = 0x3000;
+    const SIZE: u64 = 0x1000;
+    const GIC_PPI: u32 = 1;
+    const IRQ_TYPE_EDGE_RISING: u32 = 1;
+    const GIC_FDT_IRQ_PPI_CPU_SHIFT: u32 = 8;
+    // TODO(b/350498812): Rework this for >8 vCPUs.
+    const GIC_FDT_IRQ_PPI_CPU_MASK: u32 = 0xff << Self::GIC_FDT_IRQ_PPI_CPU_SHIFT;
+
+    const fn get_expected(num_cpus: usize) -> Self {
+        Self {
+            addr: Self::ADDR,
+            size: Self::SIZE,
+            irq: [
+                Self::GIC_PPI,
+                Self::IRQ_NR,
+                ((((1 << num_cpus) - 1) << Self::GIC_FDT_IRQ_PPI_CPU_SHIFT)
+                    & Self::GIC_FDT_IRQ_PPI_CPU_MASK)
+                    | Self::IRQ_TYPE_EDGE_RISING,
+            ],
+        }
+    }
+}
+
+fn read_wdt_info_from(fdt: &Fdt) -> libfdt::Result<WdtInfo> {
+    let mut node_iter = fdt.compatible_nodes(cstr!("qemu,vcpu-stall-detector"))?;
+    let node = node_iter.next().ok_or(FdtError::NotFound)?;
+    let mut ranges = node.reg()?.ok_or(FdtError::NotFound)?;
+
+    let reg = ranges.next().ok_or(FdtError::NotFound)?;
+    let size = reg.size.ok_or(FdtError::NotFound)?;
+    if ranges.next().is_some() {
+        warn!("Discarding extra vmwdt <reg> entries.");
+    }
+
+    let interrupts = node.getprop_cells(cstr!("interrupts"))?.ok_or(FdtError::NotFound)?;
+    let mut chunks = CellChunkIterator::<{ WdtInfo::IRQ_CELLS }>::new(interrupts);
+    let irq = chunks.next().ok_or(FdtError::NotFound)?;
+
+    if chunks.next().is_some() {
+        warn!("Discarding extra vmwdt <interrupts> entries.");
+    }
+
+    Ok(WdtInfo { addr: reg.addr, size, irq })
+}
+
+fn validate_wdt_info(wdt: &WdtInfo, num_cpus: usize) -> Result<(), RebootReason> {
+    if *wdt != WdtInfo::get_expected(num_cpus) {
+        error!("Invalid watchdog timer: {wdt:?}");
+        return Err(RebootReason::InvalidFdt);
+    }
+
+    Ok(())
+}
+
+fn patch_wdt_info(fdt: &mut Fdt, num_cpus: usize) -> libfdt::Result<()> {
+    let mut interrupts = WdtInfo::get_expected(num_cpus).irq;
+    for v in interrupts.iter_mut() {
+        *v = v.to_be();
+    }
+
+    let mut node = fdt
+        .root_mut()
+        .next_compatible(cstr!("qemu,vcpu-stall-detector"))?
+        .ok_or(libfdt::FdtError::NotFound)?;
+    node.setprop_inplace(cstr!("interrupts"), interrupts.as_bytes())?;
+    Ok(())
+}
+
 /// Patch the DT by deleting the ns16550a compatible nodes whose address are unknown
 fn patch_serial_info(fdt: &mut Fdt, serial_info: &SerialInfo) -> libfdt::Result<()> {
     let name = cstr!("ns16550a");
@@ -862,7 +940,9 @@
         interrupts.take(NUM_INTERRUPTS * CELLS_PER_INTERRUPT).collect();
 
     let num_cpus: u32 = num_cpus.try_into().unwrap();
+    // TODO(b/350498812): Rework this for >8 vCPUs.
     let cpu_mask: u32 = (((0x1 << num_cpus) - 1) & 0xff) << 8;
+
     for v in value.iter_mut().skip(2).step_by(CELLS_PER_INTERRUPT) {
         *v |= cpu_mask;
     }
@@ -1053,6 +1133,12 @@
     })?;
     validate_pci_info(&pci_info, &memory_range)?;
 
+    let wdt_info = read_wdt_info_from(fdt).map_err(|e| {
+        error!("Failed to read vCPU stall detector info from DT: {e}");
+        RebootReason::InvalidFdt
+    })?;
+    validate_wdt_info(&wdt_info, cpus.len())?;
+
     let serial_info = read_serial_info_from(fdt).map_err(|e| {
         error!("Failed to read serial info from DT: {e}");
         RebootReason::InvalidFdt
@@ -1141,6 +1227,10 @@
         error!("Failed to patch pci info to DT: {e}");
         RebootReason::InvalidFdt
     })?;
+    patch_wdt_info(fdt, info.cpus.len()).map_err(|e| {
+        error!("Failed to patch wdt info to DT: {e}");
+        RebootReason::InvalidFdt
+    })?;
     patch_serial_info(fdt, &info.serial_info).map_err(|e| {
         error!("Failed to patch serial info to DT: {e}");
         RebootReason::InvalidFdt
diff --git a/pvmfw/testdata/test_crosvm_dt_base.dtsi b/pvmfw/testdata/test_crosvm_dt_base.dtsi
index 7d1161a..55f0a14 100644
--- a/pvmfw/testdata/test_crosvm_dt_base.dtsi
+++ b/pvmfw/testdata/test_crosvm_dt_base.dtsi
@@ -141,6 +141,7 @@
 		reg = <0x00 0x3000 0x00 0x1000>;
 		clock-frequency = <0x0a>;
 		timeout-sec = <0x08>;
+		interrupts = <0x01 0xf 0x101>; // <GIC_PPI 0xf IRQ_TYPE_EDGE_RISING>
 	};
 
 	__symbols__ {
diff --git a/vmbase/src/bionic.rs b/vmbase/src/bionic.rs
index a38fd25..9fe3a4a 100644
--- a/vmbase/src/bionic.rs
+++ b/vmbase/src/bionic.rs
@@ -111,7 +111,7 @@
 ///
 /// # Note
 ///
-/// This Rust functions is missing the last argument of its C/C++ counterpart, a va_list.
+/// This Rust function is missing the last argument of its C/C++ counterpart, a va_list.
 #[no_mangle]
 unsafe extern "C" fn async_safe_fatal_va_list(prefix: *const c_char, format: *const c_char) {
     // SAFETY: The caller guaranteed that both strings were valid and NUL-terminated.
diff --git a/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java b/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java
index 054be51..705b168 100644
--- a/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java
+++ b/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java
@@ -198,12 +198,18 @@
         return configBuilder.build();
     }
 
+    private static boolean isVolumeKey(int keyCode) {
+        return keyCode == KeyEvent.KEYCODE_VOLUME_UP
+                || keyCode == KeyEvent.KEYCODE_VOLUME_DOWN
+                || keyCode == KeyEvent.KEYCODE_VOLUME_MUTE;
+    }
+
     @Override
     public boolean onKeyDown(int keyCode, KeyEvent event) {
         if (mVirtualMachine == null) {
             return false;
         }
-        return mVirtualMachine.sendKeyEvent(event);
+        return !isVolumeKey(keyCode) && mVirtualMachine.sendKeyEvent(event);
     }
 
     @Override
@@ -211,7 +217,7 @@
         if (mVirtualMachine == null) {
             return false;
         }
-        return mVirtualMachine.sendKeyEvent(event);
+        return !isVolumeKey(keyCode) && mVirtualMachine.sendKeyEvent(event);
     }
 
     @Override
@@ -235,41 +241,34 @@
 
                     @Override
                     public void onPayloadStarted(VirtualMachine vm) {
-                        Log.e(TAG, "payload start");
+                        // This event is only from Microdroid-based VM. Custom VM shouldn't emit
+                        // this.
                     }
 
                     @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...)");
+                        // This event is only from Microdroid-based VM. Custom VM shouldn't emit
+                        // this.
                     }
 
                     @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));
-                        }
+                        // 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.d(
-                                TAG,
-                                String.format(
-                                        "(Error occurred. code: %d, message: %s)",
-                                        errorCode, message));
+                        Log.e(TAG, "Error from VM. code: " + errorCode + " (" + message + ")");
+                        setResult(RESULT_CANCELED);
+                        finish();
                     }
 
                     @Override
                     public void onStopped(VirtualMachine vm, int reason) {
-                        Log.e(TAG, "vm stop");
+                        Log.d(TAG, "VM stopped. Reason: " + reason);
+                        setResult(RESULT_OK);
+                        finish();
                     }
                 };