Merge "guest: debian: Implement storage_balloon_agent" into main
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.kt b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.kt
index 35c5570..390ae00 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.kt
@@ -56,7 +56,6 @@
 import com.android.virtualization.terminal.VmLauncherService.VmLauncherServiceCallback
 import com.google.android.material.tabs.TabLayout
 import com.google.android.material.tabs.TabLayoutMediator
-import java.io.IOException
 import java.net.MalformedURLException
 import java.net.URL
 import java.util.concurrent.CompletableFuture
@@ -253,7 +252,7 @@
         executorService.shutdown()
         getSystemService<AccessibilityManager>(AccessibilityManager::class.java)
             .removeAccessibilityStateChangeListener(this)
-        stop(this)
+        stop(this, this)
         super.onDestroy()
     }
 
@@ -313,8 +312,6 @@
             return
         }
 
-        resizeDiskIfNecessary(image)
-
         val tapIntent = Intent(this, MainActivity::class.java)
         tapIntent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP)
         val tapPendingIntent =
@@ -362,7 +359,11 @@
                 )
                 .build()
 
-        run(this, this, notification, getDisplayInfo())
+        val diskSize = intent.getLongExtra(KEY_DISK_SIZE, image.getSize())
+        run(this, this, notification, getDisplayInfo(), diskSize).onFailure {
+            Log.e(TAG, "Failed to start VM.", it)
+            finish()
+        }
     }
 
     @VisibleForTesting
@@ -370,16 +371,6 @@
         return bootCompleted.block(timeoutMillis)
     }
 
-    private fun resizeDiskIfNecessary(image: InstalledImage) {
-        try {
-            // TODO(b/382190982): Show snackbar message instead when it's recoverable.
-            image.resize(intent.getLongExtra(KEY_DISK_SIZE, image.getSize()))
-        } catch (e: IOException) {
-            start(this, Exception("Failed to resize disk", e))
-            return
-        }
-    }
-
     companion object {
         const val TAG: String = "VmTerminalApp"
         const val KEY_DISK_SIZE: String = "disk_size"
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsDiskResizeActivity.kt b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsDiskResizeActivity.kt
index 8ea4b25..68da45f 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsDiskResizeActivity.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsDiskResizeActivity.kt
@@ -31,6 +31,7 @@
 import android.widget.TextView
 import androidx.appcompat.app.AppCompatActivity
 import androidx.core.view.isVisible
+import com.android.virtualization.terminal.VmLauncherService.VmLauncherServiceCallback
 import com.google.android.material.dialog.MaterialAlertDialogBuilder
 import java.util.Locale
 import java.util.regex.Pattern
@@ -140,12 +141,32 @@
         diskSizeMb = progressToMb(diskSizeSlider.progress)
         buttons.isVisible = false
 
-        // Restart terminal
-        val intent = baseContext.packageManager.getLaunchIntentForPackage(baseContext.packageName)
-        intent?.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
-        intent?.putExtra(MainActivity.KEY_DISK_SIZE, mbToBytes(diskSizeMb))
-        finish()
-        startActivity(intent)
+        // Note: we first stop the VM, and wait for it to fully stop. Then we (re) start the Main
+        // Activity with an extra argument specifying the new size. The actual resizing will be done
+        // there.
+        // TODO: show progress until the stop is confirmed
+        VmLauncherService.stop(
+            this,
+            object : VmLauncherServiceCallback {
+                override fun onVmStart() {}
+
+                override fun onTerminalAvailable(info: TerminalInfo) {}
+
+                override fun onVmStop() {
+                    finish()
+
+                    val intent =
+                        baseContext.packageManager.getLaunchIntentForPackage(
+                            baseContext.packageName
+                        )!!
+                    intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
+                    intent.putExtra(MainActivity.KEY_DISK_SIZE, mbToBytes(diskSizeMb))
+                    startActivity(intent)
+                }
+
+                override fun onVmError() {}
+            },
+        )
     }
 
     fun updateSliderText(sizeMb: Long) {
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt b/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt
index 0a1f0ee..01528bb 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt
@@ -15,6 +15,7 @@
  */
 package com.android.virtualization.terminal
 
+import android.app.ForegroundServiceStartNotAllowedException
 import android.app.Notification
 import android.app.NotificationManager
 import android.app.PendingIntent
@@ -66,10 +67,10 @@
 
     // TODO: using lateinit for some fields to avoid null
     private var virtualMachine: VirtualMachine? = null
-    private var resultReceiver: ResultReceiver? = null
     private var server: Server? = null
     private var debianService: DebianServiceImpl? = null
     private var portNotifier: PortNotifier? = null
+    private var runner: Runner? = null
 
     interface VmLauncherServiceCallback {
         fun onVmStart()
@@ -91,12 +92,22 @@
     }
 
     override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
+        val resultReceiver =
+            intent.getParcelableExtra<ResultReceiver?>(
+                Intent.EXTRA_RESULT_RECEIVER,
+                ResultReceiver::class.java,
+            )
+
         if (intent.action == ACTION_SHUTDOWN_VM) {
             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)
+                    stopSelf()
+                }
             } else {
                 // If there is no Debian service or it fails to shutdown, just stop the service.
                 stopSelf()
@@ -114,6 +125,11 @@
         val customImageConfigBuilder = json.toCustomImageConfigBuilder(this)
         val displaySize = intent.getParcelableExtra(EXTRA_DISPLAY_INFO, DisplayInfo::class.java)
 
+        // Note: this doesn't always do the resizing. If the current image size is the same as the
+        // requested size which is rounded up to the page alignment, resizing is not done.
+        val diskSize = intent.getLongExtra(EXTRA_DISK_SIZE, image.getSize())
+        image.resize(diskSize)
+
         customImageConfigBuilder.setAudioConfig(
             AudioConfig.Builder().setUseSpeaker(true).setUseMicrophone(true).build()
         )
@@ -122,24 +138,18 @@
         }
         val config = configBuilder.build()
 
-        val runner: Runner =
+        runner =
             try {
                 create(this, config)
             } catch (e: VirtualMachineException) {
                 throw RuntimeException("cannot create runner", e)
             }
 
-        virtualMachine = runner.vm
-        resultReceiver =
-            intent.getParcelableExtra<ResultReceiver?>(
-                Intent.EXTRA_RESULT_RECEIVER,
-                ResultReceiver::class.java,
-            )
-
+        virtualMachine = runner!!.vm
         val mbc = MemBalloonController(this, virtualMachine!!)
         mbc.start()
 
-        runner.exitStatus.thenAcceptAsync { success: Boolean ->
+        runner!!.exitStatus.thenAcceptAsync { success: Boolean ->
             mbc.stop()
             resultReceiver?.send(if (success) RESULT_STOP else RESULT_ERROR, null)
             stopSelf()
@@ -394,6 +404,7 @@
         private const val ACTION_START_VM_LAUNCHER_SERVICE =
             "android.virtualization.START_VM_LAUNCHER_SERVICE"
         const val EXTRA_DISPLAY_INFO = "EXTRA_DISPLAY_INFO"
+        const val EXTRA_DISK_SIZE = "EXTRA_DISK_SIZE"
         const val ACTION_SHUTDOWN_VM: String = "android.virtualization.ACTION_SHUTDOWN_VM"
 
         private const val RESULT_START = 0
@@ -426,7 +437,8 @@
             callback: VmLauncherServiceCallback?,
             notification: Notification?,
             displayInfo: DisplayInfo,
-        ) {
+            diskSize: Long?,
+        ): Result<Unit> {
             val i = getMyIntent(context)
             val resultReceiver: ResultReceiver =
                 object : ResultReceiver(Handler(Looper.myLooper()!!)) {
@@ -449,7 +461,15 @@
             i.putExtra(Intent.EXTRA_RESULT_RECEIVER, getResultReceiverForIntent(resultReceiver))
             i.putExtra(EXTRA_NOTIFICATION, notification)
             i.putExtra(EXTRA_DISPLAY_INFO, displayInfo)
-            context.startForegroundService(i)
+            if (diskSize != null) {
+                i.putExtra(EXTRA_DISK_SIZE, diskSize)
+            }
+            return try {
+                context.startForegroundService(i)
+                Result.success(Unit)
+            } catch (e: ForegroundServiceStartNotAllowedException) {
+                Result.failure<Unit>(e)
+            }
         }
 
         private fun getResultReceiverForIntent(r: ResultReceiver): ResultReceiver {
@@ -459,9 +479,21 @@
             return ResultReceiver.CREATOR.createFromParcel(parcel).also { parcel.recycle() }
         }
 
-        fun stop(context: Context) {
+        fun stop(context: Context, callback: VmLauncherServiceCallback?) {
             val i = getMyIntent(context)
             i.setAction(ACTION_SHUTDOWN_VM)
+            val resultReceiver: ResultReceiver =
+                object : ResultReceiver(Handler(Looper.myLooper()!!)) {
+                    override fun onReceiveResult(resultCode: Int, resultData: Bundle?) {
+                        if (callback == null) {
+                            return
+                        }
+                        when (resultCode) {
+                            RESULT_STOP -> callback.onVmStop()
+                        }
+                    }
+                }
+            i.putExtra(Intent.EXTRA_RESULT_RECEIVER, getResultReceiverForIntent(resultReceiver))
             context.startService(i)
         }
     }
diff --git a/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java b/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java
index 4294df4..4523572 100644
--- a/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java
+++ b/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java
@@ -625,7 +625,7 @@
             stdout.transferTo(out);
             stderr.transferTo(out);
             String output = out.toString("UTF-8");
-            Log.i(tag, "Got output : " + stdout);
+            Log.i(tag, "Got stdout + stderr : " + output);
             return output;
         } catch (IOException e) {
             Log.e(tag, "Error executing: " + command, e);