Refactor Memory Balloon control routines

* Created MemBalloonController as the single place for all routines
* Stop using binder; it actually wasn't a binder object.
* The periodic inflation is handled using scheduled executor service

Bug: 402025547
Bug: 392791968
Test: intentionally set the period to 5s (for testing purpose), start
the VM, go to background, and then come back. The log is as follows:

03-13 15:59:30.631  8298  8361 V VmTerminalApp: app resumed. deflating mem balloon to the minimum
03-13 15:59:51.863  8298  8361 V VmTerminalApp: inflating mem balloon to 10 %
03-13 15:59:56.863  8298  8361 V VmTerminalApp: inflating mem balloon to 15 %
03-13 16:00:01.864  8298  8361 V VmTerminalApp: inflating mem balloon to 20 %
03-13 16:00:06.864  8298  8361 V VmTerminalApp: inflating mem balloon to 25 %
03-13 16:00:08.315  8298  8361 V VmTerminalApp: app resumed. deflating mem balloon to the minimum
03-13 16:00:22.002  8298  8361 V VmTerminalApp: inflating mem balloon to 10 %
03-13 16:00:27.003  8298  8361 V VmTerminalApp: inflating mem balloon to 15 %
03-13 16:00:32.003  8298  8361 V VmTerminalApp: inflating mem balloon to 20 %
03-13 16:00:37.003  8298  8361 V VmTerminalApp: inflating mem balloon to 25 %
03-13 16:00:42.003  8298  8361 V VmTerminalApp: inflating mem balloon to 30 %

Change-Id: I05e1f8d3635d14924fc1b7b4222d7bbd7e57b6c5
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/Application.kt b/android/TerminalApp/java/com/android/virtualization/terminal/Application.kt
index c427337..efe651e 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/Application.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/Application.kt
@@ -18,21 +18,12 @@
 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() {
@@ -61,45 +52,4 @@
 
         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)
-        }
-
-        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
deleted file mode 100644
index 4e26c3c..0000000
--- a/android/TerminalApp/java/com/android/virtualization/terminal/ApplicationLifeCycleEvent.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-/*
- * 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/MemBalloonController.kt b/android/TerminalApp/java/com/android/virtualization/terminal/MemBalloonController.kt
new file mode 100644
index 0000000..e2f9add
--- /dev/null
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/MemBalloonController.kt
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 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
+
+import android.content.Context
+import android.system.virtualmachine.VirtualMachine
+import android.util.Log
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.ProcessLifecycleOwner
+import com.android.virtualization.terminal.MainActivity.Companion.TAG
+import java.util.concurrent.Executors
+import java.util.concurrent.ScheduledFuture
+import java.util.concurrent.TimeUnit
+
+/**
+ * MemBalloonController is responsible for adjusting the memory ballon size of a VM depending on
+ * whether the app is visible or running in the background
+ */
+class MemBalloonController(val context: Context, val vm: VirtualMachine) {
+    companion object {
+        private const val INITIAL_PERCENT = 10
+        private const val MAX_PERCENT = 50
+        private const val INFLATION_STEP_PERCENT = 5
+        private const val INFLATION_PERIOD_SEC = 60L
+    }
+
+    private val executor =
+        Executors.newSingleThreadScheduledExecutor(
+            TerminalThreadFactory(context.getApplicationContext())
+        )
+
+    private val observer =
+        object : DefaultLifecycleObserver {
+
+            // If the app is started or resumed, give deflate the balloon to 0 to give maximum
+            // available memory to the virtual machine
+            override fun onResume(owner: LifecycleOwner) {
+                ongoingInflation?.cancel(false)
+                executor.submit({
+                    Log.v(TAG, "app resumed. deflating mem balloon to the minimum")
+                    vm.setMemoryBalloonByPercent(0)
+                })
+            }
+
+            // If the app goes into background, progressively inflate the balloon from
+            // INITIAL_PERCENT until it reaches MAX_PERCENT
+            override fun onStop(owner: LifecycleOwner) {
+                ongoingInflation?.cancel(false)
+                balloonPercent = INITIAL_PERCENT
+                ongoingInflation =
+                    executor.scheduleAtFixedRate(
+                        {
+                            if (balloonPercent <= MAX_PERCENT) {
+                                Log.v(TAG, "inflating mem balloon to ${balloonPercent} %")
+                                vm.setMemoryBalloonByPercent(balloonPercent)
+                                balloonPercent += INFLATION_STEP_PERCENT
+                            } else {
+                                Log.v(TAG, "mem balloon is inflated to its max (${MAX_PERCENT} %)")
+                                ongoingInflation!!.cancel(false)
+                            }
+                        },
+                        0 /* initialDelay */,
+                        INFLATION_PERIOD_SEC,
+                        TimeUnit.SECONDS,
+                    )
+            }
+        }
+
+    private var balloonPercent = 0
+    private var ongoingInflation: ScheduledFuture<*>? = null
+
+    fun start() {
+        ProcessLifecycleOwner.get().lifecycle.addObserver(observer)
+    }
+
+    fun stop() {
+        ProcessLifecycleOwner.get().lifecycle.removeObserver(observer)
+        executor.shutdown()
+    }
+}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt b/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt
index 2d7468d..1857175 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt
@@ -38,7 +38,6 @@
 import android.system.virtualmachine.VirtualMachineException
 import android.util.Log
 import android.widget.Toast
-import com.android.internal.annotations.GuardedBy
 import com.android.system.virtualmachine.flags.Flags.terminalGuiSupport
 import com.android.virtualization.terminal.MainActivity.Companion.TAG
 import com.android.virtualization.terminal.Runner.Companion.create
@@ -55,7 +54,6 @@
 import java.io.File
 import java.io.FileOutputStream
 import java.io.IOException
-import java.lang.Math.min
 import java.lang.RuntimeException
 import java.net.InetSocketAddress
 import java.net.SocketAddress
@@ -66,12 +64,6 @@
 import java.util.concurrent.TimeUnit
 
 class VmLauncherService : Service() {
-    inner class VmLauncherServiceBinder : android.os.Binder() {
-        fun getService(): VmLauncherService = this@VmLauncherService
-    }
-
-    private val binder = VmLauncherServiceBinder()
-
     private lateinit var executorService: ExecutorService
 
     // TODO: using lateinit for some fields to avoid null
@@ -80,42 +72,6 @@
     private var server: Server? = null
     private var debianService: DebianServiceImpl? = null
     private var portNotifier: PortNotifier? = null
-    private var mLock = Object()
-    @GuardedBy("mLock") private var currentMemBalloonPercent = 0
-
-    @GuardedBy("mLock") private val inflateMemBalloonHandler = Handler(Looper.getMainLooper())
-    private val inflateMemBalloonTask: Runnable =
-        object : Runnable {
-            override fun run() {
-                synchronized(mLock) {
-                    if (
-                        currentMemBalloonPercent < INITIAL_MEM_BALLOON_PERCENT ||
-                            currentMemBalloonPercent > MAX_MEM_BALLOON_PERCENT
-                    ) {
-                        Log.e(
-                            TAG,
-                            "currentBalloonPercent=$currentMemBalloonPercent is invalid," +
-                                " should be in range: " +
-                                "$INITIAL_MEM_BALLOON_PERCENT~$MAX_MEM_BALLOON_PERCENT",
-                        )
-                        return
-                    }
-                    // Increases the balloon size by MEM_BALLOON_PERCENT_STEP% every time
-                    if (currentMemBalloonPercent < MAX_MEM_BALLOON_PERCENT) {
-                        currentMemBalloonPercent =
-                            min(
-                                MAX_MEM_BALLOON_PERCENT,
-                                currentMemBalloonPercent + MEM_BALLOON_PERCENT_STEP,
-                            )
-                        virtualMachine?.setMemoryBalloonByPercent(currentMemBalloonPercent)
-                        inflateMemBalloonHandler.postDelayed(
-                            this,
-                            MEM_BALLOON_INFLATE_INTERVAL_MILLIS,
-                        )
-                    }
-                }
-            }
-        }
 
     interface VmLauncherServiceCallback {
         fun onVmStart()
@@ -128,45 +84,7 @@
     }
 
     override fun onBind(intent: Intent?): IBinder? {
-        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 -> {
-                synchronized(mLock) {
-                    inflateMemBalloonHandler.removeCallbacks(inflateMemBalloonTask)
-                    currentMemBalloonPercent = 0
-                    virtualMachine?.setMemoryBalloonByPercent(currentMemBalloonPercent)
-                }
-            }
-            ApplicationLifeCycleEvent.APP_ON_STOP -> {
-                // When the app stops, inflate the memory balloon to INITIAL_MEM_BALLOON_PERCENT.
-                // Inflate the balloon by MEM_BALLOON_PERCENT_STEP every
-                // MEM_BALLOON_INFLATE_INTERVAL_MILLIS milliseconds until reaching
-                // MAX_MEM_BALLOON_PERCENT of total memory. This allows the system to reclaim
-                // memory while the app is in the background.
-                synchronized(mLock) {
-                    currentMemBalloonPercent = INITIAL_MEM_BALLOON_PERCENT
-                    virtualMachine?.setMemoryBalloonByPercent(currentMemBalloonPercent)
-                    inflateMemBalloonHandler.postDelayed(
-                        inflateMemBalloonTask,
-                        MEM_BALLOON_INFLATE_INTERVAL_MILLIS,
-                    )
-                }
-            }
-            else -> {
-                Log.e(TAG, "unrecognized lifecycle event: $event")
-            }
-        }
+        return null
     }
 
     override fun onCreate() {
@@ -220,7 +138,11 @@
                 ResultReceiver::class.java,
             )
 
+        val mbc = MemBalloonController(this, virtualMachine!!)
+        mbc.start()
+
         runner.exitStatus.thenAcceptAsync { success: Boolean ->
+            mbc.stop()
             resultReceiver?.send(if (success) RESULT_STOP else RESULT_ERROR, null)
             stopSelf()
         }
@@ -492,11 +414,6 @@
                 }
             }()
 
-        private const val INITIAL_MEM_BALLOON_PERCENT = 10
-        private const val MAX_MEM_BALLOON_PERCENT = 50
-        private const val MEM_BALLOON_INFLATE_INTERVAL_MILLIS = 60000L
-        private const val MEM_BALLOON_PERCENT_STEP = 5
-
         private fun getMyIntent(context: Context): Intent {
             return Intent(context.getApplicationContext(), VmLauncherService::class.java)
         }