Merge "Update tab title whenever ttyd session title changes" into main
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/Logger.kt b/android/TerminalApp/java/com/android/virtualization/terminal/Logger.kt
index 4162247..ba03716 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/Logger.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/Logger.kt
@@ -47,6 +47,10 @@
         }
 
         try {
+            if (Files.isRegularFile(dir)) {
+                Log.i(tag, "Removed legacy log file: $dir")
+                Files.delete(dir)
+            }
             Files.createDirectories(dir)
             deleteOldLogs(dir, 10)
             val logPath = dir.resolve(LocalDateTime.now().toString() + ".txt")
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 e5cabbf..067d540 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt
@@ -63,7 +63,10 @@
 import java.util.concurrent.TimeUnit
 
 class VmLauncherService : Service() {
-    private lateinit var executorService: ExecutorService
+    // Thread pool
+    private lateinit var bgThreads: ExecutorService
+    // Single thread
+    private lateinit var mainWorkerThread: ExecutorService
     private lateinit var image: InstalledImage
 
     // TODO: using lateinit for some fields to avoid null
@@ -89,7 +92,9 @@
 
     override fun onCreate() {
         super.onCreate()
-        executorService = Executors.newCachedThreadPool(TerminalThreadFactory(applicationContext))
+        val threadFactory = TerminalThreadFactory(getApplicationContext())
+        bgThreads = Executors.newCachedThreadPool(threadFactory)
+        mainWorkerThread = Executors.newSingleThreadExecutor(threadFactory)
         image = InstalledImage.getDefault(this)
     }
 
@@ -116,11 +121,15 @@
                 // done.
                 val diskSize = intent.getLongExtra(EXTRA_DISK_SIZE, image.getSize())
 
-                executorService.submit({
+                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 -> executorService.submit({ doShutdown(resultReceiver) })
+            ACTION_SHUTDOWN_VM -> mainWorkerThread.submit({ doShutdown(resultReceiver) })
             else -> {
                 Log.e(TAG, "Unknown command " + intent.action)
                 stopSelf()
@@ -137,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)
@@ -163,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 ->
@@ -172,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, executorService)
-
-        startForeground(this.hashCode(), notification)
+        val logDir = getFileStreamPath(virtualMachine.name + ".log").toPath()
+        Logger.setup(virtualMachine, logDir, bgThreads)
 
         resultReceiver.send(RESULT_START, null)
 
@@ -192,7 +194,7 @@
                     resultReceiver.send(RESULT_TERMINAL_AVAIL, bundle)
                     startDebianServer(ipAddress)
                 },
-                executorService,
+                bgThreads,
             )
             .exceptionallyAsync(
                 { e ->
@@ -201,7 +203,7 @@
                     stopSelf()
                     null
                 },
-                executorService,
+                bgThreads,
             )
     }
 
@@ -368,7 +370,7 @@
             return
         }
 
-        executorService.execute(
+        bgThreads.execute(
             Runnable {
                 // TODO(b/373533555): we can use mDNS for that.
                 val debianServicePortFile = File(filesDir, "debian_service_port")
@@ -388,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()
         }
     }
@@ -411,21 +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
-        }
-        executorService.shutdownNow()
+        bgThreads.shutdownNow()
+        mainWorkerThread.shutdown()
         super.onDestroy()
     }