Snap for 13166373 from 52c37f9363e776c5cecf796a1e92029a99e67103 to 25Q2-release

Change-Id: Ib54867d16caec0e2067a0287e9bd0f37e5d577c1
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/Application.kt b/android/TerminalApp/java/com/android/virtualization/terminal/Application.kt
index efe651e..9f4909d 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/Application.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/Application.kt
@@ -18,12 +18,21 @@
 import android.app.Application as AndroidApplication
 import android.app.NotificationChannel
 import android.app.NotificationManager
+import android.content.ComponentName
 import android.content.Context
+import android.content.Intent
+import android.content.ServiceConnection
+import android.os.IBinder
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.ProcessLifecycleOwner
 
 public class Application : AndroidApplication() {
     override fun onCreate() {
         super.onCreate()
         setupNotificationChannels()
+        val lifecycleObserver = ApplicationLifecycleObserver()
+        ProcessLifecycleOwner.get().lifecycle.addObserver(lifecycleObserver)
     }
 
     private fun setupNotificationChannels() {
@@ -52,4 +61,53 @@
 
         fun getInstance(c: Context): Application = c.getApplicationContext() as Application
     }
+
+    /**
+     * Observes application lifecycle events and interacts with the VmLauncherService to manage
+     * virtual machine state based on application lifecycle transitions. This class binds to the
+     * VmLauncherService and notifies it of application lifecycle events (onStart, onStop), allowing
+     * the service to manage the VM accordingly.
+     */
+    inner class ApplicationLifecycleObserver() : DefaultLifecycleObserver {
+        private var vmLauncherService: VmLauncherService? = null
+        private val connection =
+            object : ServiceConnection {
+                override fun onServiceConnected(className: ComponentName, service: IBinder) {
+                    val binder = service as VmLauncherService.VmLauncherServiceBinder
+                    vmLauncherService = binder.getService()
+                }
+
+                override fun onServiceDisconnected(arg0: ComponentName) {
+                    vmLauncherService = null
+                }
+            }
+
+        override fun onCreate(owner: LifecycleOwner) {
+            super.onCreate(owner)
+            bindToVmLauncherService()
+        }
+
+        override fun onStart(owner: LifecycleOwner) {
+            super.onStart(owner)
+            vmLauncherService?.processAppLifeCycleEvent(ApplicationLifeCycleEvent.APP_ON_START)
+        }
+
+        override fun onStop(owner: LifecycleOwner) {
+            vmLauncherService?.processAppLifeCycleEvent(ApplicationLifeCycleEvent.APP_ON_STOP)
+            super.onStop(owner)
+        }
+
+        override fun onDestroy(owner: LifecycleOwner) {
+            if (vmLauncherService != null) {
+                this@Application.unbindService(connection)
+                vmLauncherService = null
+            }
+            super.onDestroy(owner)
+        }
+
+        fun bindToVmLauncherService() {
+            val intent = Intent(this@Application, VmLauncherService::class.java)
+            this@Application.bindService(intent, connection, 0) // No BIND_AUTO_CREATE
+        }
+    }
 }
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/ApplicationLifeCycleEvent.kt b/android/TerminalApp/java/com/android/virtualization/terminal/ApplicationLifeCycleEvent.kt
new file mode 100644
index 0000000..4e26c3c
--- /dev/null
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/ApplicationLifeCycleEvent.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2025 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
+
+enum class ApplicationLifeCycleEvent {
+    APP_ON_START,
+    APP_ON_STOP,
+}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/ImageArchive.kt b/android/TerminalApp/java/com/android/virtualization/terminal/ImageArchive.kt
index 017ff89..be1f922 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/ImageArchive.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/ImageArchive.kt
@@ -65,6 +65,14 @@
         }
     }
 
+    /** Returns path to the archive. */
+    fun getPath(): String {
+        return when (source) {
+            is UrlSource -> source.value.toString()
+            is PathSource -> source.value.toString()
+        }
+    }
+
     /** Returns size of the archive in bytes */
     @Throws(IOException::class)
     fun getSize(): Long {
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/InstallerService.kt b/android/TerminalApp/java/com/android/virtualization/terminal/InstallerService.kt
index 7180e87..01c3880 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/InstallerService.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/InstallerService.kt
@@ -150,21 +150,26 @@
 
     private fun downloadFromSdcard(): Boolean {
         val archive = fromSdCard()
+        val archive_path = archive.getPath()
 
         // Installing from sdcard is preferred, but only supported only in debuggable build.
-        if (Build.isDebuggable() && archive.exists()) {
-            Log.i(TAG, "trying to install /sdcard/linux/images.tar.gz")
+        if (!Build.isDebuggable()) {
+            Log.i(TAG, "Non-debuggable build doesn't support installation from $archive_path")
+            return false
+        }
+        if (!archive.exists()) {
+            return false
+        }
 
-            val dest = getDefault(this).installDir
-            try {
-                archive.installTo(dest, null)
-                Log.i(TAG, "image is installed from /sdcard/linux/images.tar.gz")
-                return true
-            } catch (e: IOException) {
-                Log.i(TAG, "Failed to install /sdcard/linux/images.tar.gz", e)
-            }
-        } else {
-            Log.i(TAG, "Non-debuggable build doesn't support installation from /sdcard/linux")
+        Log.i(TAG, "trying to install $archive_path")
+
+        val dest = getDefault(this).installDir
+        try {
+            archive.installTo(dest, null)
+            Log.i(TAG, "image is installed from $archive_path")
+            return true
+        } catch (e: IOException) {
+            Log.i(TAG, "Failed to install $archive_path", e)
         }
         return false
     }
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt b/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt
index 6301da4..4bfad62 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt
@@ -62,6 +62,12 @@
 import java.util.concurrent.Executors
 
 class VmLauncherService : Service() {
+    inner class VmLauncherServiceBinder : android.os.Binder() {
+        fun getService(): VmLauncherService = this@VmLauncherService
+    }
+
+    private val binder = VmLauncherServiceBinder()
+
     // TODO: using lateinit for some fields to avoid null
     private var executorService: ExecutorService? = null
     private var virtualMachine: VirtualMachine? = null
@@ -79,7 +85,32 @@
     }
 
     override fun onBind(intent: Intent?): IBinder? {
-        return null
+        return binder
+    }
+
+    /**
+     * Processes application lifecycle events and adjusts the virtual machine's memory balloon
+     * accordingly.
+     *
+     * @param event The application lifecycle event.
+     */
+    fun processAppLifeCycleEvent(event: ApplicationLifeCycleEvent) {
+        when (event) {
+            // When the app starts, reset the memory balloon to 0%.
+            // This gives the app maximum available memory.
+            ApplicationLifeCycleEvent.APP_ON_START -> {
+                virtualMachine?.setMemoryBalloonByPercent(0)
+            }
+            ApplicationLifeCycleEvent.APP_ON_STOP -> {
+                // When the app stops, inflate the memory balloon to 10%.
+                // This allows the system to reclaim memory while the app is in the background.
+                // TODO(b/400590341) Inflate the balloon while the application remains Stop status.
+                virtualMachine?.setMemoryBalloonByPercent(10)
+            }
+            else -> {
+                Log.e(TAG, "unrecognized lifecycle event: $event")
+            }
+        }
     }
 
     override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
diff --git a/android/virtmgr/src/aidl.rs b/android/virtmgr/src/aidl.rs
index 3c5408c..1c4c2eb 100644
--- a/android/virtmgr/src/aidl.rs
+++ b/android/virtmgr/src/aidl.rs
@@ -935,6 +935,20 @@
             })
             .collect::<binder::Result<_>>()?;
 
+        let memory_reclaim_supported =
+            system_properties::read_bool("hypervisor.memory_reclaim.supported", false)
+                .unwrap_or(false);
+
+        let balloon = config.balloon && memory_reclaim_supported;
+
+        if !balloon {
+            warn!(
+                "Memory balloon not enabled:
+                config.balloon={},hypervisor.memory_reclaim.supported={}",
+                config.balloon, memory_reclaim_supported
+            );
+        }
+
         // Actually start the VM.
         let crosvm_config = CrosvmConfig {
             cid,
@@ -974,7 +988,7 @@
             boost_uclamp: config.boostUclamp,
             gpu_config,
             audio_config,
-            balloon: config.balloon,
+            balloon,
             usb_config,
             dump_dt_fd,
             enable_hypervisor_specific_auth_method: config.enableHypervisorSpecificAuthMethod,
@@ -1446,7 +1460,7 @@
     calling_partition: CallingPartition,
 ) -> Result<()> {
     let path = format!("/proc/self/fd/{}", fd.as_raw_fd());
-    let link = fs::read_link(&path).context(format!("can't read_link {path}"))?;
+    let link = fs::read_link(&path).with_context(|| format!("can't read_link {path}"))?;
 
     // microdroid vendor image is OK
     if cfg!(vendor_modules) && link == Path::new("/vendor/etc/avf/microdroid/microdroid_vendor.img")
@@ -1454,7 +1468,10 @@
         return Ok(());
     }
 
-    let is_fd_vendor = link.starts_with("/vendor") || link.starts_with("/odm");
+    let fd_partition = find_partition(Some(&link))
+        .with_context(|| format!("can't find_partition {}", link.display()))?;
+    let is_fd_vendor =
+        fd_partition == CallingPartition::Vendor || fd_partition == CallingPartition::Odm;
     let is_caller_vendor =
         calling_partition == CallingPartition::Vendor || calling_partition == CallingPartition::Odm;
 
@@ -1730,6 +1747,10 @@
             .or_service_specific_exception(-1)
     }
 
+    fn isMemoryBalloonEnabled(&self) -> binder::Result<bool> {
+        Ok(self.instance.balloon_enabled)
+    }
+
     fn getMemoryBalloon(&self) -> binder::Result<i64> {
         let balloon = self
             .instance
diff --git a/android/virtmgr/src/crosvm.rs b/android/virtmgr/src/crosvm.rs
index 77710c3..5f81e90 100644
--- a/android/virtmgr/src/crosvm.rs
+++ b/android/virtmgr/src/crosvm.rs
@@ -417,6 +417,8 @@
     pub vm_service: Mutex<Option<Strong<dyn IVirtualMachineService>>>,
     /// Recorded metrics of VM such as timestamp or cpu / memory usage.
     pub vm_metric: Mutex<VmMetric>,
+    // Whether virtio-balloon is enabled
+    pub balloon_enabled: bool,
     /// The latest lifecycle state which the payload reported itself to be in.
     payload_state: Mutex<PayloadState>,
     /// Represents the condition that payload_state was updated
@@ -449,6 +451,7 @@
         let cid = config.cid;
         let name = config.name.clone();
         let protected = config.protected;
+        let balloon_enabled = config.balloon;
         let requester_uid_name = User::from_uid(Uid::from_raw(requester_uid))
             .ok()
             .flatten()
@@ -469,6 +472,7 @@
             payload_state: Mutex::new(PayloadState::Starting),
             payload_state_updated: Condvar::new(),
             requester_uid_name,
+            balloon_enabled,
         };
         info!("{} created", &instance);
         Ok(instance)
@@ -722,6 +726,9 @@
 
     /// Returns current virtio-balloon size.
     pub fn get_memory_balloon(&self) -> Result<u64, Error> {
+        if !self.balloon_enabled {
+            bail!("virtio-balloon is not enabled");
+        }
         let socket_path_cstring = path_to_cstring(&self.crosvm_control_socket_path);
         let mut balloon_actual = 0u64;
         // SAFETY: Pointers are valid for the lifetime of the call. Null `stats` is valid.
@@ -741,6 +748,9 @@
     /// Inflates the virtio-balloon by `num_bytes` to reclaim guest memory. Called in response to
     /// memory-trimming notifications.
     pub fn set_memory_balloon(&self, num_bytes: u64) -> Result<(), Error> {
+        if !self.balloon_enabled {
+            bail!("virtio-balloon is not enabled");
+        }
         let socket_path_cstring = path_to_cstring(&self.crosvm_control_socket_path);
         // SAFETY: Pointer is valid for the lifetime of the call.
         let success = unsafe {
@@ -1038,8 +1048,7 @@
         .arg("--cid")
         .arg(config.cid.to_string());
 
-    if system_properties::read_bool("hypervisor.memory_reclaim.supported", false)? && config.balloon
-    {
+    if config.balloon {
         command.arg("--balloon-page-reporting");
     } else {
         command.arg("--no-balloon");
diff --git a/android/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachine.aidl b/android/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachine.aidl
index a01d385..e7aeefd 100644
--- a/android/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachine.aidl
+++ b/android/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachine.aidl
@@ -49,6 +49,7 @@
     void stop();
 
     /** Access to the VM's memory balloon. */
+    boolean isMemoryBalloonEnabled();
     long getMemoryBalloon();
     void setMemoryBalloon(long num_bytes);
 
diff --git a/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachine.java b/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachine.java
index 0445fcb..40050c0 100644
--- a/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachine.java
+++ b/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachine.java
@@ -288,17 +288,7 @@
                 percent = 50;
             }
 
-            synchronized (mLock) {
-                try {
-                    if (mVirtualMachine != null) {
-                        long bytes = mConfig.getMemoryBytes();
-                        mVirtualMachine.setMemoryBalloon(bytes * percent / 100);
-                    }
-                } catch (Exception e) {
-                    /* Caller doesn't want our exceptions. Log them instead. */
-                    Log.w(TAG, "TrimMemory failed: ", e);
-                }
-            }
+            setMemoryBalloonByPercent(percent);
         }
     }
 
@@ -1392,6 +1382,24 @@
         }
     }
 
+    /** @hide */
+    public void setMemoryBalloonByPercent(int percent) {
+        if (percent < 0 || percent > 100) {
+            Log.e(TAG, String.format("Invalid percent value: %d", percent));
+            return;
+        }
+        synchronized (mLock) {
+            try {
+                if (mVirtualMachine != null && mVirtualMachine.isMemoryBalloonEnabled()) {
+                    long bytes = mConfig.getMemoryBytes();
+                    mVirtualMachine.setMemoryBalloon(bytes * percent / 100);
+                }
+            } catch (RemoteException | ServiceSpecificException 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());
diff --git a/tests/vts/AndroidTest.xml b/tests/vts/AndroidTest.xml
index 6926f9f..a59f161 100644
--- a/tests/vts/AndroidTest.xml
+++ b/tests/vts/AndroidTest.xml
@@ -21,7 +21,7 @@
     <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
         <option name="cleanup" value="true" />
         <option name="push" value="vts_libavf_test->/data/nativetest64/vendor/vts_libavf_test" />
-        <option name="push" value="rialto.bin->/data/local/tmp/rialto.bin" />
+        <option name="push" value="rialto.bin->/data/nativetest64/vendor/rialto.bin" />
     </target_preparer>
 
     <object type="module_controller" class="com.android.tradefed.testtype.suite.module.ArchModuleController">
diff --git a/tests/vts/src/vts_libavf_test.rs b/tests/vts/src/vts_libavf_test.rs
index dc37aad..c13b510 100644
--- a/tests/vts/src/vts_libavf_test.rs
+++ b/tests/vts/src/vts_libavf_test.rs
@@ -75,7 +75,7 @@
 
 fn run_rialto(protected_vm: bool) -> Result<()> {
     let kernel_file =
-        File::open("/data/local/tmp/rialto.bin").context("Failed to open kernel file")?;
+        File::open("/data/nativetest64/vendor/rialto.bin").context("Failed to open kernel file")?;
     let kernel_fd = kernel_file.into_raw_fd();
 
     // SAFETY: AVirtualMachineRawConfig_create() isn't unsafe but rust_bindgen forces it to be seen