Do the clean-up in the worker thread

The clean-up is done in the worker thread even when the service gets an
unexpected stop.

In addition a few more refactoring was done.

* the field virtualMachine is removed as it's accessible via the runner
  field.

* startForeground is called outside of the worker thread which can block
  for a long time, and eventually causes the foreground service timeout
exeception.

Bug: 401835074
Test: run, rerun, resize, close from notification
Change-Id: I89c9416f5942227b22c0207bd2eba812087248d1
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/Runner.kt b/android/TerminalApp/java/com/android/virtualization/terminal/Runner.kt
index 6454cbd..642cb26 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/Runner.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/Runner.kt
@@ -27,7 +27,7 @@
 import java.util.concurrent.ForkJoinPool
 
 /** Utility class for creating a VM and waiting for it to finish. */
-internal class Runner private constructor(val vm: VirtualMachine?, callback: Callback) {
+internal class Runner private constructor(val vm: VirtualMachine, callback: Callback) {
     /** Get future about VM's exit status. */
     val exitStatus = callback.finishedSuccessfully
 
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt b/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt
index 9020458..067d540 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt
@@ -124,6 +124,10 @@
                 mainWorkerThread.submit({
                     doStart(notification, displayInfo, diskSize, resultReceiver)
                 })
+
+                // Do this outside of the main worker thread, so that we don't cause
+                // ForegroundServiceDidNotStartInTimeException
+                startForeground(this.hashCode(), notification)
             }
             ACTION_SHUTDOWN_VM -> mainWorkerThread.submit({ doShutdown(resultReceiver) })
             else -> {
@@ -142,11 +146,6 @@
         diskSize: Long,
         resultReceiver: ResultReceiver,
     ) {
-        if (virtualMachine != null) {
-            Log.d(TAG, "VM instance is already started")
-            return
-        }
-
         val image = InstalledImage.getDefault(this)
         val json = ConfigJson.from(this, image.configPath)
         val configBuilder = json.toConfigBuilder(this)
@@ -168,8 +167,8 @@
                 throw RuntimeException("cannot create runner", e)
             }
 
-        virtualMachine = runner!!.vm
-        val mbc = MemBalloonController(this, virtualMachine!!)
+        val virtualMachine = runner!!.vm
+        val mbc = MemBalloonController(this, virtualMachine)
         mbc.start()
 
         runner!!.exitStatus.thenAcceptAsync { success: Boolean ->
@@ -177,10 +176,8 @@
             resultReceiver.send(if (success) RESULT_STOP else RESULT_ERROR, null)
             stopSelf()
         }
-        val logDir = getFileStreamPath(virtualMachine!!.name + ".log").toPath()
-        Logger.setup(virtualMachine!!, logDir, bgThreads)
-
-        startForeground(this.hashCode(), notification)
+        val logDir = getFileStreamPath(virtualMachine.name + ".log").toPath()
+        Logger.setup(virtualMachine, logDir, bgThreads)
 
         resultReceiver.send(RESULT_START, null)
 
@@ -393,18 +390,21 @@
     }
 
     @WorkerThread
-    private fun doShutdown(resultReceiver: ResultReceiver) {
+    private fun doShutdown(resultReceiver: ResultReceiver?) {
+        stopForeground(STOP_FOREGROUND_REMOVE)
         if (debianService != null && debianService!!.shutdownDebian()) {
             // During shutdown, change the notification content to indicate that it's closing
             val notification = createNotificationForTerminalClose()
             getSystemService<NotificationManager?>(NotificationManager::class.java)
                 .notify(this.hashCode(), notification)
             runner?.exitStatus?.thenAcceptAsync { success: Boolean ->
-                resultReceiver.send(if (success) RESULT_STOP else RESULT_ERROR, null)
+                resultReceiver?.send(if (success) RESULT_STOP else RESULT_ERROR, null)
                 stopSelf()
             }
+            runner = null
         } else {
             // If there is no Debian service or it fails to shutdown, just stop the service.
+            runner?.vm?.stop()
             stopSelf()
         }
     }
@@ -416,22 +416,16 @@
     }
 
     override fun onDestroy() {
+        mainWorkerThread.submit({
+            if (runner?.vm?.getStatus() == VirtualMachine.STATUS_RUNNING) {
+                doShutdown(null)
+            }
+        })
         portNotifier?.stop()
         getSystemService<NotificationManager?>(NotificationManager::class.java).cancelAll()
         stopDebianServer()
-        if (virtualMachine != null) {
-            if (virtualMachine!!.getStatus() == VirtualMachine.STATUS_RUNNING) {
-                try {
-                    virtualMachine!!.stop()
-                    stopForeground(STOP_FOREGROUND_REMOVE)
-                } catch (e: VirtualMachineException) {
-                    Log.e(TAG, "failed to stop a VM instance", e)
-                }
-            }
-            virtualMachine = null
-        }
         bgThreads.shutdownNow()
-        mainWorkerThread.shutdownNow()
+        mainWorkerThread.shutdown()
         super.onDestroy()
     }