Merge "Speak something for the invisible element" into main
diff --git a/android/TerminalApp/Android.bp b/android/TerminalApp/Android.bp
index 2711af0..733a72b 100644
--- a/android/TerminalApp/Android.bp
+++ b/android/TerminalApp/Android.bp
@@ -9,7 +9,6 @@
         "java/**/*.kt",
     ],
     resource_dirs: ["res"],
-    asset_dirs: ["assets"],
     static_libs: [
         "androidx-constraintlayout_constraintlayout",
         "androidx.window_window",
diff --git a/android/TerminalApp/assets/.gitkeep b/android/TerminalApp/assets/.gitkeep
deleted file mode 100644
index e69de29..0000000
--- a/android/TerminalApp/assets/.gitkeep
+++ /dev/null
diff --git a/android/TerminalApp/assets/client.p12 b/android/TerminalApp/assets/client.p12
deleted file mode 100644
index f1f5820..0000000
--- a/android/TerminalApp/assets/client.p12
+++ /dev/null
Binary files differ
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/ImageArchive.java b/android/TerminalApp/java/com/android/virtualization/terminal/ImageArchive.java
new file mode 100644
index 0000000..12f485a
--- /dev/null
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/ImageArchive.java
@@ -0,0 +1,116 @@
+/*
+ * 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.terminal;
+
+import android.os.Build;
+import android.os.Environment;
+
+import org.apache.commons.compress.archivers.ArchiveEntry;
+import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
+import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
+
+import java.io.BufferedInputStream;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.util.Arrays;
+import java.util.function.Function;
+
+/**
+ * ImageArchive models the archive file (images.tar.gz) where VM payload files are in. This class
+ * provides methods for handling the archive file, most importantly installing it.
+ */
+class ImageArchive {
+    private static final String DIR_IN_SDCARD = "linux";
+    private static final String ARCHIVE_NAME = "images.tar.gz";
+    private static final String HOST_URL = "https://dl.google.com/android/ferrochrome/latest";
+
+    // Only one can be non-null
+    private final URL mUrl;
+    private final Path mPath;
+
+    private ImageArchive(URL url) {
+        mUrl = url;
+        mPath = null;
+    }
+
+    private ImageArchive(Path path) {
+        mUrl = null;
+        mPath = path;
+    }
+
+    /** Creates ImageArchive which is located in the sdcard. This archive is for testing only. */
+    public static ImageArchive fromSdCard() {
+        Path dir = Environment.getExternalStoragePublicDirectory(DIR_IN_SDCARD).toPath();
+        Path file = dir.resolve(ARCHIVE_NAME);
+        return new ImageArchive(file);
+    }
+
+    /** Creates ImageArchive which is hosted in the Google server. This is the official archive. */
+    public static ImageArchive fromInternet() {
+        String arch = Arrays.asList(Build.SUPPORTED_ABIS).contains("x86_64") ? "x86_64" : "aarch64";
+        try {
+            URL url = new URL(HOST_URL + "/" + arch + "/" + ARCHIVE_NAME);
+            return new ImageArchive(url);
+        } catch (MalformedURLException e) {
+            // cannot happen
+            throw new RuntimeException(e);
+        }
+    }
+
+    private InputStream getInputStream(Function<InputStream, InputStream> filter)
+            throws IOException {
+        InputStream is = mPath != null ? new FileInputStream(mPath.toFile()) : mUrl.openStream();
+        BufferedInputStream bufStream = new BufferedInputStream(is);
+        return filter == null ? bufStream : filter.apply(bufStream);
+    }
+
+    /**
+     * Installs this ImageArchive to a directory pointed by path. filter can be supplied to provide
+     * an additional input stream which will be used during the installation.
+     */
+    public void installTo(Path path, Function<InputStream, InputStream> filter) throws IOException {
+        try (InputStream stream = getInputStream(filter);
+                GzipCompressorInputStream gzStream = new GzipCompressorInputStream(stream);
+                TarArchiveInputStream tarStream = new TarArchiveInputStream(gzStream)) {
+
+            Files.createDirectories(path);
+            ArchiveEntry entry;
+            while ((entry = tarStream.getNextEntry()) != null) {
+                Path to = path.resolve(entry.getName());
+                if (Files.isDirectory(to)) {
+                    Files.createDirectories(to);
+                } else {
+                    Files.copy(tarStream, to, StandardCopyOption.REPLACE_EXISTING);
+                }
+            }
+        }
+        postInstall();
+    }
+
+    // To save storage, delete the source archive on the disk.
+    private void postInstall() throws IOException {
+        if (mPath != null) {
+            Files.deleteIfExists(mPath);
+        }
+    }
+}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/InstallerService.java b/android/TerminalApp/java/com/android/virtualization/terminal/InstallerService.java
index 4b2e640..a8b4ca2 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/InstallerService.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/InstallerService.java
@@ -64,9 +64,6 @@
                     ? "https://dl.google.com/android/ferrochrome/latest/x86_64/images.tar.gz"
                     : "https://dl.google.com/android/ferrochrome/latest/aarch64/images.tar.gz";
 
-    private static final String SELINUX_FILE_CONTEXT =
-            "u:object_r:virtualizationservice_data_file:";
-
     private final Object mLock = new Object();
 
     private Notification mNotification;
@@ -162,9 +159,6 @@
         mExecutorService.execute(
                 () -> {
                     boolean success = downloadFromSdcard() || downloadFromUrl(isWifiOnly);
-                    if (success) {
-                        reLabelImagesSELinuxContext();
-                    }
                     stopForeground(STOP_FOREGROUND_REMOVE);
 
                     synchronized (mLock) {
@@ -176,24 +170,6 @@
                 });
     }
 
-    private void reLabelImagesSELinuxContext() {
-        File payloadFolder = InstallUtils.getInternalStorageDir(this).toFile();
-
-        // The context should be u:object_r:privapp_data_file:s0:c35,c257,c512,c768
-        // and we want to get s0:c35,c257,c512,c768 part
-        String level = SELinux.getFileContext(payloadFolder.toString()).split(":", 4)[3];
-        String targetContext = SELINUX_FILE_CONTEXT + level;
-
-        File[] files = payloadFolder.listFiles();
-        for (File file : files) {
-            if (file.isFile() &&
-                    !Objects.equals(SELinux.getFileContext(file.toString()),
-                            targetContext)) {
-                SELinux.setFileContext(file.toString(), targetContext);
-            }
-        }
-    }
-
     private boolean downloadFromSdcard() {
         // Installing from sdcard is preferred, but only supported only in debuggable build.
         if (Build.isDebuggable()) {
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsPortForwardingActivity.kt b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsPortForwardingActivity.kt
index 1b39ff0..a332a9d 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsPortForwardingActivity.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsPortForwardingActivity.kt
@@ -47,7 +47,7 @@
                 HashSet<String>()
             )
 
-        for (port in ports!!) {
+        for (port in ports!!.sortedWith(compareBy( { it.toInt() } ))) {
             val enabled =
                 sharedPref.getBoolean(
                     getString(R.string.preference_forwarding_port_is_enabled) + port,
diff --git a/android/virtmgr/src/aidl.rs b/android/virtmgr/src/aidl.rs
index 1cae344..9a733b6 100644
--- a/android/virtmgr/src/aidl.rs
+++ b/android/virtmgr/src/aidl.rs
@@ -573,41 +573,42 @@
                 .or_binder_exception(ExceptionCode::SECURITY)?;
         }
 
-        // Check if partition images are labeled incorrectly. This is to prevent random images
-        // which are not protected by the Android Verified Boot (e.g. bits downloaded by apps) from
-        // being loaded in a pVM. This applies to everything but the instance image in the raw
-        // config, and everything but the non-executable, generated partitions in the app
-        // config.
-        config
-            .disks
-            .iter()
-            .flat_map(|disk| disk.partitions.iter())
-            .filter(|partition| {
-                if is_app_config {
-                    !is_safe_app_partition(&partition.label)
-                } else {
-                    !is_safe_raw_partition(&partition.label)
-                }
-            })
-            .try_for_each(check_label_for_partition)
-            .or_service_specific_exception(-1)?;
+        let kernel = maybe_clone_file(&config.kernel)?;
+        let initrd = maybe_clone_file(&config.initrd)?;
+
+        if config.protectedVm {
+            // Fail fast with a meaningful error message in case device doesn't support pVMs.
+            check_protected_vm_is_supported()?;
+
+            // In a protected VM, we require custom kernels to come from a trusted source
+            // (b/237054515).
+            check_label_for_kernel_files(&kernel, &initrd).or_service_specific_exception(-1)?;
+
+            // Check if partition images are labeled incorrectly. This is to prevent random images
+            // which are not protected by the Android Verified Boot (e.g. bits downloaded by apps)
+            // from being loaded in a pVM. This applies to everything but the instance image in the
+            // raw config, and everything but the non-executable, generated partitions in the app
+            // config.
+            config
+                .disks
+                .iter()
+                .flat_map(|disk| disk.partitions.iter())
+                .filter(|partition| {
+                    if is_app_config {
+                        !is_safe_app_partition(&partition.label)
+                    } else {
+                        !is_safe_raw_partition(&partition.label)
+                    }
+                })
+                .try_for_each(check_label_for_partition)
+                .or_service_specific_exception(-1)?;
+        }
 
         // Check if files for payloads and bases are NOT coming from /vendor and /odm, as they may
         // have unstable interfaces.
         // TODO(b/316431494): remove once Treble interfaces are stabilized.
         check_partitions_for_files(config).or_service_specific_exception(-1)?;
 
-        let kernel = maybe_clone_file(&config.kernel)?;
-        let initrd = maybe_clone_file(&config.initrd)?;
-
-        if config.protectedVm {
-            // In a protected VM, we require custom kernels to come from a trusted source
-            // (b/237054515).
-            check_label_for_kernel_files(&kernel, &initrd).or_service_specific_exception(-1)?;
-            // Fail fast with a meaningful error message in case device doesn't support pVMs.
-            check_protected_vm_is_supported()?;
-        }
-
         let zero_filler_path = temporary_directory.join("zero.img");
         write_zero_filler(&zero_filler_path)
             .context("Failed to make composite image")
diff --git a/build/debian/fai_config/scripts/AVF/10-systemd b/build/debian/fai_config/scripts/AVF/10-systemd
index a514299..94838bc 100755
--- a/build/debian/fai_config/scripts/AVF/10-systemd
+++ b/build/debian/fai_config/scripts/AVF/10-systemd
@@ -10,3 +10,5 @@
 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
+
+sed -i 's/#LLMNR=yes/LLMNR=no/' $target/etc/systemd/resolved.conf
diff --git a/guest/forwarder_guest_launcher/src/main.rs b/guest/forwarder_guest_launcher/src/main.rs
index 1da37b4..f6944d6 100644
--- a/guest/forwarder_guest_launcher/src/main.rs
+++ b/guest/forwarder_guest_launcher/src/main.rs
@@ -35,6 +35,7 @@
 }
 
 const NON_PREVILEGED_PORT_RANGE_START: i32 = 1024;
+const TTYD_PORT: i32 = 7681;
 const TCPSTATES_IP_4: i8 = 4;
 const TCPSTATES_STATE_CLOSE: &str = "CLOSE";
 const TCPSTATES_STATE_LISTEN: &str = "LISTEN";
@@ -108,6 +109,10 @@
     Ok(())
 }
 
+fn is_forwardable_port(port: i32) -> bool {
+    port >= NON_PREVILEGED_PORT_RANGE_START && port != TTYD_PORT
+}
+
 async fn report_active_ports(
     mut client: DebianServiceClient<Channel>,
 ) -> Result<(), Box<dyn std::error::Error>> {
@@ -130,7 +135,7 @@
         .map(|x| x.socket)
         .filter(|x| x.is_ipv4())
         .map(|x| x.port().into())
-        .filter(|x| *x >= NON_PREVILEGED_PORT_RANGE_START) // Ignore privileged ports
+        .filter(|x| is_forwardable_port(*x))
         .collect();
     send_active_ports_report(listening_ports.clone(), &mut client).await?;
 
@@ -140,7 +145,7 @@
         if row.ip != TCPSTATES_IP_4 {
             continue;
         }
-        if row.lport < NON_PREVILEGED_PORT_RANGE_START {
+        if !is_forwardable_port(row.lport) {
             continue;
         }
         if row.rport > 0 {