Merge "Import translations. DO NOT MERGE ANYWHERE" into main
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/DebianServiceImpl.java b/android/TerminalApp/java/com/android/virtualization/terminal/DebianServiceImpl.java
index d167da3..9cf6093 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/DebianServiceImpl.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/DebianServiceImpl.java
@@ -30,6 +30,8 @@
 import com.android.virtualization.terminal.proto.ReportVmActivePortsRequest;
 import com.android.virtualization.terminal.proto.ReportVmActivePortsResponse;
 import com.android.virtualization.terminal.proto.ReportVmIpAddrResponse;
+import com.android.virtualization.terminal.proto.ShutdownQueueOpeningRequest;
+import com.android.virtualization.terminal.proto.ShutdownRequestItem;
 
 import io.grpc.stub.StreamObserver;
 
@@ -41,6 +43,7 @@
     private final PortsStateManager mPortsStateManager;
     private PortsStateManager.Listener mPortsStateListener;
     private final DebianServiceCallback mCallback;
+    private Runnable mShutdownRunnable;
 
     static {
         System.loadLibrary("forwarder_host_jni");
@@ -93,6 +96,28 @@
         responseObserver.onCompleted();
     }
 
+    public boolean shutdownDebian() {
+        if (mShutdownRunnable == null) {
+            Log.d(TAG, "mShutdownRunnable is not ready.");
+            return false;
+        }
+        mShutdownRunnable.run();
+        return true;
+    }
+
+    @Override
+    public void openShutdownRequestQueue(
+            ShutdownQueueOpeningRequest request,
+            StreamObserver<ShutdownRequestItem> responseObserver) {
+        Log.d(TAG, "openShutdownRequestQueue");
+        mShutdownRunnable =
+                () -> {
+                    responseObserver.onNext(ShutdownRequestItem.newBuilder().build());
+                    responseObserver.onCompleted();
+                    mShutdownRunnable = null;
+                };
+    }
+
     @Keep
     private static class ForwarderHostCallback {
         private StreamObserver<ForwardingRequestItem> mResponseObserver;
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/ImageArchive.java b/android/TerminalApp/java/com/android/virtualization/terminal/ImageArchive.java
index 7f14179..b9910f7 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/ImageArchive.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/ImageArchive.java
@@ -46,7 +46,7 @@
 class ImageArchive {
     private static final String DIR_IN_SDCARD = "linux";
     private static final String ARCHIVE_NAME = "images.tar.gz";
-    private static final String BUILD_TAG = "latest"; // TODO: use actual tag name
+    private static final String BUILD_TAG = Integer.toString(Build.VERSION.SDK_INT_FULL);
     private static final String HOST_URL = "https://dl.google.com/android/ferrochrome/" + BUILD_TAG;
 
     // Only one can be non-null
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.java b/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.java
index a82c688..6d2c5bd 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.java
@@ -54,7 +54,6 @@
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.Objects;
-import java.util.Set;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 
@@ -144,7 +143,10 @@
     @Override
     public int onStartCommand(Intent intent, int flags, int startId) {
         if (Objects.equals(intent.getAction(), ACTION_STOP_VM_LAUNCHER_SERVICE)) {
-            stopSelf();
+            // If there is no Debian service or it fails to shutdown, just stop the service.
+            if (mDebianService == null || !mDebianService.shutdownDebian()) {
+                stopSelf();
+            }
             return START_NOT_STICKY;
         }
         if (mVirtualMachine != null) {
@@ -293,12 +295,15 @@
 
     public static void stop(Context context) {
         Intent i = getMyIntent(context);
-        context.stopService(i);
+        i.setAction(VmLauncherService.ACTION_STOP_VM_LAUNCHER_SERVICE);
+        context.startService(i);
     }
 
     @Override
     public void onDestroy() {
-        mPortNotifier.stop();
+        if (mPortNotifier != null) {
+            mPortNotifier.stop();
+        }
         getSystemService(NotificationManager.class).cancelAll();
         stopDebianServer();
         if (mVirtualMachine != null) {
diff --git a/build/debian/build.sh b/build/debian/build.sh
index 9104adc..19894c2 100755
--- a/build/debian/build.sh
+++ b/build/debian/build.sh
@@ -192,6 +192,7 @@
 	build_rust_binary_and_copy forwarder_guest
 	build_rust_binary_and_copy forwarder_guest_launcher
 	build_rust_binary_and_copy ip_addr_reporter
+	build_rust_binary_and_copy shutdown_runner
 }
 
 run_fai() {
diff --git a/build/debian/fai_config/files/etc/systemd/system/shutdown_runner.service/AVF b/build/debian/fai_config/files/etc/systemd/system/shutdown_runner.service/AVF
new file mode 100644
index 0000000..bfb8afb
--- /dev/null
+++ b/build/debian/fai_config/files/etc/systemd/system/shutdown_runner.service/AVF
@@ -0,0 +1,11 @@
+[Unit]
+After=syslog.target
+After=network.target
+After=virtiofs_internal.service
+[Service]
+ExecStart=/usr/bin/bash -c '/usr/local/bin/shutdown_runner --grpc_port $(cat /mnt/internal/debian_service_port)'
+Type=simple
+User=root
+Group=root
+[Install]
+WantedBy=multi-user.target
diff --git a/build/debian/fai_config/scripts/AVF/10-systemd b/build/debian/fai_config/scripts/AVF/10-systemd
index 94838bc..119bec7 100755
--- a/build/debian/fai_config/scripts/AVF/10-systemd
+++ b/build/debian/fai_config/scripts/AVF/10-systemd
@@ -3,6 +3,7 @@
 chmod +x $target/usr/local/bin/forwarder_guest
 chmod +x $target/usr/local/bin/forwarder_guest_launcher
 chmod +x $target/usr/local/bin/ip_addr_reporter
+chmod +x $target/usr/local/bin/shutdown_runner
 chmod +x $target/usr/local/bin/ttyd
 ln -s /etc/systemd/system/ttyd.service $target/etc/systemd/system/multi-user.target.wants/ttyd.service
 ln -s /etc/systemd/system/ip_addr_reporter.service $target/etc/systemd/system/multi-user.target.wants/ip_addr_reporter.service
@@ -10,5 +11,6 @@
 ln -s /etc/systemd/system/forwarder_guest_launcher.service $target/etc/systemd/system/multi-user.target.wants/forwarder_guest_launcher.service
 ln -s /etc/systemd/system/virtiofs_internal.service $target/etc/systemd/system/multi-user.target.wants/virtiofs_internal.service
 ln -s /etc/systemd/system/backup_mount.service $target/etc/systemd/system/multi-user.target.wants/backup_mount.service
+ln -s /etc/systemd/system/shutdown_runner.service $target/etc/systemd/system/multi-user.target.wants/shutdown_runner.service
 
 sed -i 's/#LLMNR=yes/LLMNR=no/' $target/etc/systemd/resolved.conf
diff --git a/build/debian/release.sh b/build/debian/release.sh
index 437f9c8..8f89e21 100755
--- a/build/debian/release.sh
+++ b/build/debian/release.sh
@@ -83,7 +83,7 @@
 	local image=$(get_image_path ${arch} ${build_id})
 
 	local tag=${tag:-${build_id}}
-	local serving_url=/android/ferrochrome/${arch}/${tag}/${image_filename}
+	local serving_url=/android/ferrochrome/${tag}/${arch}/${image_filename}
 	echo "Releasing ${image} to ${serving_url}"
 
 	local request='payload : { url_path: '"\"${serving_url}\""' source_path : '"\"${image}\""' }'
diff --git a/guest/pvmfw/src/entry.rs b/guest/pvmfw/src/entry.rs
index 2f0b391..343c2fc 100644
--- a/guest/pvmfw/src/entry.rs
+++ b/guest/pvmfw/src/entry.rs
@@ -81,7 +81,9 @@
     // - can't access MMIO (except the console, already configured by vmbase)
 
     match main_wrapper(fdt_address as usize, payload_start as usize, payload_size as usize) {
-        Ok((entry, bcc)) => jump_to_payload(fdt_address, entry.try_into().unwrap(), bcc),
+        Ok((entry, bcc, keep_uart)) => {
+            jump_to_payload(fdt_address, entry.try_into().unwrap(), bcc, keep_uart)
+        }
         Err(e) => {
             const REBOOT_REASON_CONSOLE: usize = 1;
             console_writeln!(REBOOT_REASON_CONSOLE, "{}", e.as_avf_reboot_string());
@@ -100,7 +102,7 @@
     fdt: usize,
     payload: usize,
     payload_size: usize,
-) -> Result<(usize, Range<usize>), RebootReason> {
+) -> Result<(usize, Range<usize>, bool), RebootReason> {
     // Limitations in this function:
     // - only access MMIO once (and while) it has been mapped and configured
     // - only perform logging once the logger has been initialized
@@ -136,6 +138,8 @@
         config_entries.bcc,
         config_entries.debug_policy,
     )?;
+    // Keep UART MMIO_GUARD-ed for debuggable payloads, to enable earlycon.
+    let keep_uart = cfg!(debuggable_vms_improvements) && debuggable_payload;
 
     // Writable-dirty regions will be flushed when MemoryTracker is dropped.
     config_entries.bcc.zeroize();
@@ -144,24 +148,18 @@
         error!("Failed to unshare MMIO ranges: {e}");
         RebootReason::InternalError
     })?;
-    // Call unshare_all_memory here (instead of relying on the dtor) while UART is still mapped.
     unshare_all_memory();
 
-    if cfg!(debuggable_vms_improvements) && debuggable_payload {
-        // Keep UART MMIO_GUARD-ed for debuggable payloads, to enable earlycon.
-    } else {
-        unshare_uart().map_err(|e| {
-            error!("Failed to unshare the UART: {e}");
-            RebootReason::InternalError
-        })?;
+    Ok((slices.kernel.as_ptr() as usize, next_bcc, keep_uart))
+}
+
+fn jump_to_payload(fdt_address: u64, payload_start: u64, bcc: Range<usize>, keep_uart: bool) -> ! {
+    if !keep_uart {
+        unshare_uart().unwrap();
     }
 
     deactivate_dynamic_page_tables();
 
-    Ok((slices.kernel.as_ptr() as usize, next_bcc))
-}
-
-fn jump_to_payload(fdt_address: u64, payload_start: u64, bcc: Range<usize>) -> ! {
     const ASM_STP_ALIGN: usize = size_of::<u64>() * 2;
     const SCTLR_EL1_RES1: u64 = (0b11 << 28) | (0b101 << 20) | (0b1 << 11);
     // Stage 1 instruction access cacheability is unaffected.
diff --git a/guest/shutdown_runner/.gitignore b/guest/shutdown_runner/.gitignore
new file mode 100644
index 0000000..ea8c4bf
--- /dev/null
+++ b/guest/shutdown_runner/.gitignore
@@ -0,0 +1 @@
+/target
diff --git a/guest/shutdown_runner/Cargo.toml b/guest/shutdown_runner/Cargo.toml
new file mode 100644
index 0000000..b74e7ee
--- /dev/null
+++ b/guest/shutdown_runner/Cargo.toml
@@ -0,0 +1,17 @@
+[package]
+name = "shutdown_runner"
+version = "0.1.0"
+edition = "2021"
+license = "Apache-2.0"
+
+[dependencies]
+anyhow = "1.0.94"
+clap = { version = "4.5.20", features = ["derive"] }
+log = "0.4.22"
+netdev = "0.31.0"
+prost = "0.13.3"
+tokio = { version = "1.40.0", features = ["rt-multi-thread"] }
+tonic = "0.12.3"
+
+[build-dependencies]
+tonic-build = "0.12.3"
diff --git a/guest/shutdown_runner/build.rs b/guest/shutdown_runner/build.rs
new file mode 100644
index 0000000..e3939d4
--- /dev/null
+++ b/guest/shutdown_runner/build.rs
@@ -0,0 +1,7 @@
+fn main() -> Result<(), Box<dyn std::error::Error>> {
+    let proto_file = "../../libs/debian_service/proto/DebianService.proto";
+
+    tonic_build::compile_protos(proto_file).unwrap();
+
+    Ok(())
+}
diff --git a/guest/shutdown_runner/src/main.rs b/guest/shutdown_runner/src/main.rs
new file mode 100644
index 0000000..19e9883
--- /dev/null
+++ b/guest/shutdown_runner/src/main.rs
@@ -0,0 +1,46 @@
+use api::debian_service_client::DebianServiceClient;
+use api::ShutdownQueueOpeningRequest;
+use std::process::Command;
+
+use anyhow::anyhow;
+use clap::Parser;
+use log::debug;
+pub mod api {
+    tonic::include_proto!("com.android.virtualization.terminal.proto");
+}
+
+#[derive(Parser)]
+/// Flags for running command
+pub struct Args {
+    /// grpc port number
+    #[arg(long)]
+    #[arg(alias = "grpc_port")]
+    grpc_port: String,
+}
+
+#[tokio::main]
+async fn main() -> Result<(), Box<dyn std::error::Error>> {
+    let args = Args::parse();
+    let gateway_ip_addr = netdev::get_default_gateway()?.ipv4[0];
+
+    let server_addr = format!("http://{}:{}", gateway_ip_addr.to_string(), args.grpc_port);
+
+    debug!("connect to grpc server {}", server_addr);
+
+    let mut client = DebianServiceClient::connect(server_addr).await.map_err(|e| e.to_string())?;
+
+    let mut res_stream = client
+        .open_shutdown_request_queue(tonic::Request::new(ShutdownQueueOpeningRequest {}))
+        .await?
+        .into_inner();
+
+    while let Some(_response) = res_stream.message().await? {
+        let status = Command::new("poweroff").status().expect("power off");
+        if !status.success() {
+            return Err(anyhow!("Failed to power off: {status}").into());
+        }
+        debug!("poweroff");
+        break;
+    }
+    Ok(())
+}
diff --git a/libs/debian_service/proto/DebianService.proto b/libs/debian_service/proto/DebianService.proto
index 61bcece..739f0ac 100644
--- a/libs/debian_service/proto/DebianService.proto
+++ b/libs/debian_service/proto/DebianService.proto
@@ -25,6 +25,7 @@
   rpc ReportVmActivePorts (ReportVmActivePortsRequest) returns (ReportVmActivePortsResponse) {}
   rpc ReportVmIpAddr (IpAddr) returns (ReportVmIpAddrResponse) {}
   rpc OpenForwardingRequestQueue (QueueOpeningRequest) returns (stream ForwardingRequestItem) {}
+  rpc OpenShutdownRequestQueue (ShutdownQueueOpeningRequest) returns (stream ShutdownRequestItem) {}
 }
 
 message QueueOpeningRequest {
@@ -51,3 +52,7 @@
   int32 guest_tcp_port = 1;
   int32 vsock_port = 2;
 }
+
+message ShutdownQueueOpeningRequest {}
+
+message ShutdownRequestItem {}
\ No newline at end of file
diff --git a/tests/pvmfw/java/com/android/pvmfw/test/CustomPvmfwHostTestCaseBase.java b/tests/pvmfw/java/com/android/pvmfw/test/CustomPvmfwHostTestCaseBase.java
index 3116cc5..296604b 100644
--- a/tests/pvmfw/java/com/android/pvmfw/test/CustomPvmfwHostTestCaseBase.java
+++ b/tests/pvmfw/java/com/android/pvmfw/test/CustomPvmfwHostTestCaseBase.java
@@ -52,6 +52,7 @@
     public static final String VM_REFERENCE_DT_PATH = "/data/local/tmp/pvmfw/reference_dt.dtb";
 
     @NonNull public static final String MICRODROID_LOG_PATH = TEST_ROOT + "log.txt";
+    @NonNull public static final String MICRODROID_CONSOLE_PATH = TEST_ROOT + "console.txt";
     public static final int BOOT_COMPLETE_TIMEOUT_MS = 30000; // 30 seconds
     public static final int BOOT_FAILURE_WAIT_TIME_MS = 10000; // 10 seconds
     public static final int CONSOLE_OUTPUT_WAIT_MS = 5000; // 5 seconds
diff --git a/tests/pvmfw/java/com/android/pvmfw/test/DebugPolicyHostTests.java b/tests/pvmfw/java/com/android/pvmfw/test/DebugPolicyHostTests.java
index 7efbbc7..6a28d5e 100644
--- a/tests/pvmfw/java/com/android/pvmfw/test/DebugPolicyHostTests.java
+++ b/tests/pvmfw/java/com/android/pvmfw/test/DebugPolicyHostTests.java
@@ -248,6 +248,8 @@
                         "run-app",
                         "--log",
                         MICRODROID_LOG_PATH,
+                        "--console",
+                        MICRODROID_CONSOLE_PATH,
                         "--protected",
                         getPathForPackage(PACKAGE_NAME),
                         TEST_ROOT + "idsig",