Merge "Implement memory ballooning in Terminal App" into main
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/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/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());