Merge "rialto: move aarch64 library to target-specific section" into main
diff --git a/android/TerminalApp/Android.bp b/android/TerminalApp/Android.bp
index 2bac412..c18ada4 100644
--- a/android/TerminalApp/Android.bp
+++ b/android/TerminalApp/Android.bp
@@ -14,7 +14,9 @@
         // TODO(b/330257000): will be removed when binder RPC is used
         "android.system.virtualizationservice_internal-java",
         "androidx-constraintlayout_constraintlayout",
+        "androidx.navigation_navigation-fragment-ktx",
         "androidx.window_window",
+        "androidx.work_work-runtime",
         "apache-commons-compress",
         "avf_aconfig_flags_java",
         "com.google.android.material_material",
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/ConfigJson.kt b/android/TerminalApp/java/com/android/virtualization/terminal/ConfigJson.kt
index 1fd58cd..5d22790 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/ConfigJson.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/ConfigJson.kt
@@ -29,7 +29,6 @@
 import com.android.virtualization.terminal.ConfigJson.InputJson
 import com.android.virtualization.terminal.ConfigJson.PartitionJson
 import com.android.virtualization.terminal.ConfigJson.SharedPathJson
-import com.android.virtualization.terminal.InstalledImage.Companion.getDefault
 import com.google.gson.Gson
 import com.google.gson.annotations.SerializedName
 import java.io.BufferedReader
@@ -313,7 +312,7 @@
         private fun replaceKeywords(r: Reader, context: Context): String {
             val rules: Map<String, String> =
                 mapOf(
-                    "\\\$PAYLOAD_DIR" to getDefault(context).installDir.toString(),
+                    "\\\$PAYLOAD_DIR" to InstalledImage.getDefault(context).installDir.toString(),
                     "\\\$USER_ID" to context.userId.toString(),
                     "\\\$PACKAGE_NAME" to context.getPackageName(),
                     "\\\$APP_DATA_DIR" to context.getDataDir().toString(),
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/DebianServiceImpl.kt b/android/TerminalApp/java/com/android/virtualization/terminal/DebianServiceImpl.kt
index e035ad4..2c52283 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/DebianServiceImpl.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/DebianServiceImpl.kt
@@ -18,9 +18,9 @@
 import android.content.Context
 import android.util.Log
 import androidx.annotation.Keep
-import com.android.virtualization.terminal.DebianServiceImpl.ForwarderHostCallback
+import com.android.internal.annotations.GuardedBy
+import com.android.system.virtualmachine.flags.Flags
 import com.android.virtualization.terminal.MainActivity.Companion.TAG
-import com.android.virtualization.terminal.PortsStateManager.Companion.getInstance
 import com.android.virtualization.terminal.proto.DebianServiceGrpc.DebianServiceImplBase
 import com.android.virtualization.terminal.proto.ForwardingRequestItem
 import com.android.virtualization.terminal.proto.QueueOpeningRequest
@@ -28,13 +28,17 @@
 import com.android.virtualization.terminal.proto.ReportVmActivePortsResponse
 import com.android.virtualization.terminal.proto.ShutdownQueueOpeningRequest
 import com.android.virtualization.terminal.proto.ShutdownRequestItem
+import com.android.virtualization.terminal.proto.StorageBalloonQueueOpeningRequest
+import com.android.virtualization.terminal.proto.StorageBalloonRequestItem
 import io.grpc.stub.ServerCallStreamObserver
 import io.grpc.stub.StreamObserver
 
 internal class DebianServiceImpl(context: Context) : DebianServiceImplBase() {
-    private val portsStateManager: PortsStateManager = getInstance(context)
+    private val portsStateManager = PortsStateManager.getInstance(context)
     private var portsStateListener: PortsStateManager.Listener? = null
     private var shutdownRunnable: Runnable? = null
+    private val mLock = Object()
+    @GuardedBy("mLock") private var storageBalloonCallback: StorageBalloonCallback? = null
 
     override fun reportVmActivePorts(
         request: ReportVmActivePortsRequest,
@@ -80,10 +84,9 @@
         request: ShutdownQueueOpeningRequest?,
         responseObserver: StreamObserver<ShutdownRequestItem?>,
     ) {
-        val serverCallStreamObserver = responseObserver as ServerCallStreamObserver<ShutdownRequestItem?>
-        serverCallStreamObserver.setOnCancelHandler {
-            shutdownRunnable = null
-        }
+        val serverCallStreamObserver =
+            responseObserver as ServerCallStreamObserver<ShutdownRequestItem?>
+        serverCallStreamObserver.setOnCancelHandler { shutdownRunnable = null }
         Log.d(TAG, "openShutdownRequestQueue")
         shutdownRunnable = Runnable {
             if (serverCallStreamObserver.isCancelled()) {
@@ -95,6 +98,60 @@
         }
     }
 
+    private class StorageBalloonCallback(
+        private val responseObserver: StreamObserver<StorageBalloonRequestItem?>
+    ) {
+        fun setAvailableStorageBytes(availableBytes: Long) {
+            Log.d(TAG, "send setStorageBalloon: $availableBytes")
+            val item =
+                StorageBalloonRequestItem.newBuilder().setAvailableBytes(availableBytes).build()
+            responseObserver.onNext(item)
+        }
+
+        fun closeConnection() {
+            Log.d(TAG, "close StorageBalloonQueue")
+            responseObserver.onCompleted()
+        }
+    }
+
+    fun setAvailableStorageBytes(availableBytes: Long): Boolean {
+        synchronized(mLock) {
+            if (storageBalloonCallback == null) {
+                Log.d(TAG, "storageBalloonCallback is not ready.")
+                return false
+            }
+            storageBalloonCallback!!.setAvailableStorageBytes(availableBytes)
+        }
+        return true
+    }
+
+    override fun openStorageBalloonRequestQueue(
+        request: StorageBalloonQueueOpeningRequest?,
+        responseObserver: StreamObserver<StorageBalloonRequestItem?>,
+    ) {
+        if (!Flags.terminalStorageBalloon()) {
+            return
+        }
+        Log.d(TAG, "openStorageRequestQueue")
+        synchronized(mLock) {
+            if (storageBalloonCallback != null) {
+                Log.d(TAG, "RequestQueue already exists. Closing connection.")
+                storageBalloonCallback!!.closeConnection()
+            }
+            storageBalloonCallback = StorageBalloonCallback(responseObserver)
+        }
+    }
+
+    fun closeStorageBalloonRequestQueue() {
+        Log.d(TAG, "Stopping storage balloon queue")
+        synchronized(mLock) {
+            if (storageBalloonCallback != null) {
+                storageBalloonCallback!!.closeConnection()
+                storageBalloonCallback = null
+            }
+        }
+    }
+
     @Keep
     private class ForwarderHostCallback(
         private val responseObserver: StreamObserver<ForwardingRequestItem?>
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/InstalledImage.kt b/android/TerminalApp/java/com/android/virtualization/terminal/InstalledImage.kt
index 7acc5f3..a4663c8 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/InstalledImage.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/InstalledImage.kt
@@ -116,11 +116,12 @@
         val roundedUpDesiredSize = roundUp(desiredSize)
         val curSize = getSize()
 
+        runE2fsck(rootPartition)
+
         if (roundedUpDesiredSize == curSize) {
             return roundedUpDesiredSize
         }
 
-        runE2fsck(rootPartition)
         if (roundedUpDesiredSize > curSize) {
             allocateSpace(rootPartition, roundedUpDesiredSize)
         }
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/MainActivity.kt b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.kt
index 390ae00..e4eaecb 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.kt
@@ -15,6 +15,7 @@
  */
 package com.android.virtualization.terminal
 
+import android.app.ForegroundServiceStartNotAllowedException
 import android.app.Notification
 import android.app.PendingIntent
 import android.content.Context
@@ -44,15 +45,11 @@
 import androidx.activity.result.ActivityResultCallback
 import androidx.activity.result.ActivityResultLauncher
 import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
-import androidx.lifecycle.ViewModelProvider
+import androidx.activity.viewModels
 import androidx.viewpager2.widget.ViewPager2
 import com.android.internal.annotations.VisibleForTesting
 import com.android.microdroid.test.common.DeviceProperties
 import com.android.system.virtualmachine.flags.Flags.terminalGuiSupport
-import com.android.virtualization.terminal.ErrorActivity.Companion.start
-import com.android.virtualization.terminal.InstalledImage.Companion.getDefault
-import com.android.virtualization.terminal.VmLauncherService.Companion.run
-import com.android.virtualization.terminal.VmLauncherService.Companion.stop
 import com.android.virtualization.terminal.VmLauncherService.VmLauncherServiceCallback
 import com.google.android.material.tabs.TabLayout
 import com.google.android.material.tabs.TabLayoutMediator
@@ -75,17 +72,17 @@
     private lateinit var image: InstalledImage
     private lateinit var accessibilityManager: AccessibilityManager
     private lateinit var manageExternalStorageActivityResultLauncher: ActivityResultLauncher<Intent>
-    private lateinit var terminalViewModel: TerminalViewModel
     private lateinit var viewPager: ViewPager2
     private lateinit var tabLayout: TabLayout
     private lateinit var terminalTabAdapter: TerminalTabAdapter
     private val terminalInfo = CompletableFuture<TerminalInfo>()
+    private val terminalViewModel: TerminalViewModel by viewModels()
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         lockOrientationIfNecessary()
 
-        image = getDefault(this)
+        image = InstalledImage.getDefault(this)
 
         val launchInstaller = installIfNecessary()
 
@@ -114,7 +111,6 @@
     }
 
     private fun initializeUi() {
-        terminalViewModel = ViewModelProvider(this)[TerminalViewModel::class.java]
         setContentView(R.layout.activity_headless)
         tabLayout = findViewById<TabLayout>(R.id.tab_layout)
         displayMenu = findViewById<Button>(R.id.display_button)
@@ -154,6 +150,20 @@
         TabLayoutMediator(tabLayout, viewPager, false, false) { _: TabLayout.Tab?, _: Int -> }
             .attach()
 
+        tabLayout.addOnTabSelectedListener(
+            object : TabLayout.OnTabSelectedListener {
+                override fun onTabSelected(tab: TabLayout.Tab?) {
+                    tab?.position?.let {
+                        terminalViewModel.selectedTabViewId = terminalTabAdapter.tabs[it].id
+                    }
+                }
+
+                override fun onTabUnselected(tab: TabLayout.Tab?) {}
+
+                override fun onTabReselected(tab: TabLayout.Tab?) {}
+            }
+        )
+
         addTerminalTab()
 
         tabAddButton?.setOnClickListener { addTerminalTab() }
@@ -163,7 +173,9 @@
         val tab = tabLayout.newTab()
         tab.setCustomView(R.layout.tabitem_terminal)
         viewPager.offscreenPageLimit += 1
-        terminalTabAdapter.addTab()
+        val tabId = terminalTabAdapter.addTab()
+        terminalViewModel.selectedTabViewId = tabId
+        terminalViewModel.terminalTabs[tabId] = tab
         tab.customView!!
             .findViewById<Button>(R.id.tab_close_button)
             .setOnClickListener(
@@ -198,7 +210,7 @@
     override fun dispatchKeyEvent(event: KeyEvent): Boolean {
         if (Build.isDebuggable() && event.keyCode == KeyEvent.KEYCODE_UNKNOWN) {
             if (event.action == KeyEvent.ACTION_UP) {
-                start(this, Exception("Debug: KeyEvent.KEYCODE_UNKNOWN"))
+                ErrorActivity.start(this, Exception("Debug: KeyEvent.KEYCODE_UNKNOWN"))
             }
             return true
         }
@@ -226,9 +238,7 @@
                 "&fontWeightBold=" +
                 (FontStyle.FONT_WEIGHT_BOLD + config.fontWeightAdjustment) +
                 "&screenReaderMode=" +
-                accessibilityManager.isEnabled +
-                "&titleFixed=" +
-                getString(R.string.app_name))
+                accessibilityManager.isEnabled)
 
         try {
             return URL("https", ipAddress, port, "/$query")
@@ -252,7 +262,8 @@
         executorService.shutdown()
         getSystemService<AccessibilityManager>(AccessibilityManager::class.java)
             .removeAccessibilityStateChangeListener(this)
-        stop(this, this)
+        val intent = VmLauncherService.getIntentForShutdown(this, this)
+        startService(intent)
         super.onDestroy()
     }
 
@@ -272,7 +283,7 @@
     override fun onVmError() {
         Log.i(TAG, "onVmError()")
         // TODO: error cause is too simple.
-        start(this, Exception("onVmError"))
+        ErrorActivity.start(this, Exception("onVmError"))
     }
 
     override fun onAccessibilityStateChanged(enabled: Boolean) {
@@ -307,7 +318,7 @@
     }
 
     private fun startVm() {
-        val image = getDefault(this)
+        val image = InstalledImage.getDefault(this)
         if (!image.isInstalled()) {
             return
         }
@@ -322,9 +333,7 @@
         val settingsPendingIntent =
             PendingIntent.getActivity(this, 0, settingsIntent, PendingIntent.FLAG_IMMUTABLE)
 
-        val stopIntent = Intent()
-        stopIntent.setClass(this, VmLauncherService::class.java)
-        stopIntent.setAction(VmLauncherService.ACTION_SHUTDOWN_VM)
+        val stopIntent = VmLauncherService.getIntentForShutdown(this, this)
         val stopPendingIntent =
             PendingIntent.getService(
                 this,
@@ -359,9 +368,20 @@
                 )
                 .build()
 
-        val diskSize = intent.getLongExtra(KEY_DISK_SIZE, image.getSize())
-        run(this, this, notification, getDisplayInfo(), diskSize).onFailure {
-            Log.e(TAG, "Failed to start VM.", it)
+        val diskSize = intent.getLongExtra(EXTRA_DISK_SIZE, image.getSize())
+
+        val intent =
+            VmLauncherService.getIntentForStart(
+                this,
+                this,
+                notification,
+                getDisplayInfo(),
+                diskSize,
+            )
+        try {
+            startForegroundService(intent)
+        } catch (e: ForegroundServiceStartNotAllowedException) {
+            Log.e(TAG, "Failed to start VM", e)
             finish()
         }
     }
@@ -373,7 +393,8 @@
 
     companion object {
         const val TAG: String = "VmTerminalApp"
-        const val KEY_DISK_SIZE: String = "disk_size"
+        const val PREFIX: String = "com.android.virtualization.terminal."
+        const val EXTRA_DISK_SIZE: String = PREFIX + "EXTRA_DISK_SIZE"
         private val TERMINAL_CONNECTION_TIMEOUT_MS: Int
         private const val REQUEST_CODE_INSTALLER = 0x33
         private const val FONT_SIZE_DEFAULT = 13
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/SettingsDiskResizeActivity.kt b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsDiskResizeActivity.kt
index 68da45f..af1ae95 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsDiskResizeActivity.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsDiskResizeActivity.kt
@@ -145,28 +145,30 @@
         // 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() {}
+        val intent =
+            VmLauncherService.getIntentForShutdown(
+                this,
+                object : VmLauncherServiceCallback {
+                    override fun onVmStart() {}
 
-                override fun onTerminalAvailable(info: TerminalInfo) {}
+                    override fun onTerminalAvailable(info: TerminalInfo) {}
 
-                override fun onVmStop() {
-                    finish()
+                    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)
-                }
+                        val intent =
+                            baseContext.packageManager.getLaunchIntentForPackage(
+                                baseContext.packageName
+                            )!!
+                        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
+                        intent.putExtra(MainActivity.EXTRA_DISK_SIZE, mbToBytes(diskSizeMb))
+                        startActivity(intent)
+                    }
 
-                override fun onVmError() {}
-            },
-        )
+                    override fun onVmError() {}
+                },
+            )
+        startService(intent)
     }
 
     fun updateSliderText(sizeMb: Long) {
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/StorageBalloonWorker.kt b/android/TerminalApp/java/com/android/virtualization/terminal/StorageBalloonWorker.kt
new file mode 100644
index 0000000..345bf75
--- /dev/null
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/StorageBalloonWorker.kt
@@ -0,0 +1,111 @@
+/*
+ * 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.os.storage.StorageManager
+import android.os.storage.StorageManager.UUID_DEFAULT
+import android.util.Log
+import androidx.work.WorkManager
+import androidx.work.Worker
+import androidx.work.WorkerParameters
+import com.android.virtualization.terminal.MainActivity.Companion.TAG
+import java.util.concurrent.TimeUnit
+
+class StorageBalloonWorker(appContext: Context, workerParams: WorkerParameters) :
+    Worker(appContext, workerParams) {
+
+    override fun doWork(): Result {
+        Log.d(TAG, "StorageBalloonWorker.doWork() called")
+
+        var storageManager =
+            applicationContext.getSystemService(Context.STORAGE_SERVICE) as StorageManager
+        val hostAllocatableBytes = storageManager.getAllocatableBytes(UUID_DEFAULT)
+
+        val guestAvailableBytes = calculateGuestAvailableStorageSize(hostAllocatableBytes)
+        // debianService must be set when this function is called.
+        debianService!!.setAvailableStorageBytes(guestAvailableBytes)
+
+        val delaySeconds = calculateDelaySeconds(hostAllocatableBytes)
+        scheduleNextTask(delaySeconds)
+
+        return Result.success()
+    }
+
+    private fun calculateGuestAvailableStorageSize(hostAllocatableBytes: Long): Long {
+        return hostAllocatableBytes - HOST_RESERVED_BYTES
+    }
+
+    private fun calculateDelaySeconds(hostAvailableBytes: Long): Long {
+        return when {
+            hostAvailableBytes < CRITICAL_STORAGE_THRESHOLD_BYTES -> CRITICAL_DELAY_SECONDS
+            hostAvailableBytes < LOW_STORAGE_THRESHOLD_BYTES -> LOW_STORAGE_DELAY_SECONDS
+            hostAvailableBytes < MODERATE_STORAGE_THRESHOLD_BYTES -> MODERATE_STORAGE_DELAY_SECONDS
+            else -> NORMAL_DELAY_SECONDS
+        }
+    }
+
+    private fun scheduleNextTask(delaySeconds: Long) {
+        val storageBalloonTaskRequest =
+            androidx.work.OneTimeWorkRequest.Builder(StorageBalloonWorker::class.java)
+                .setInitialDelay(delaySeconds, TimeUnit.SECONDS)
+                .build()
+        androidx.work.WorkManager.getInstance(applicationContext)
+            .enqueueUniqueWork(
+                "storageBalloonTask",
+                androidx.work.ExistingWorkPolicy.REPLACE,
+                storageBalloonTaskRequest,
+            )
+        Log.d(TAG, "next storage balloon task is scheduled in $delaySeconds seconds")
+    }
+
+    companion object {
+        private var debianService: DebianServiceImpl? = null
+
+        // Reserve 1GB as host-only region.
+        private const val HOST_RESERVED_BYTES = 1024L * 1024 * 1024
+
+        // Thresholds for deciding time period to report storage information to the guest.
+        // Less storage is available on the host, more frequently the host will report storage
+        // information to the guest.
+        //
+        // Critical: (host storage < 1GB) => report every 5 seconds
+        private const val CRITICAL_STORAGE_THRESHOLD_BYTES = 1L * 1024 * 1024 * 1024
+        private const val CRITICAL_DELAY_SECONDS = 5L
+        // Low: (1GB <= storage < 5GB) => report every 60 seconds
+        private const val LOW_STORAGE_THRESHOLD_BYTES = 5L * 1024 * 1024 * 1024
+        private const val LOW_STORAGE_DELAY_SECONDS = 60L
+        // Moderate: (5GB <= storage < 10GB) => report every 15 minutes
+        private const val MODERATE_STORAGE_THRESHOLD_BYTES = 10L * 1024 * 1024 * 1024
+        private const val MODERATE_STORAGE_DELAY_SECONDS = 15L * 60
+        // Normal: report every 60 minutes
+        private const val NORMAL_DELAY_SECONDS = 60L * 60
+
+        internal fun start(ctx: Context, ds: DebianServiceImpl) {
+            debianService = ds
+            val storageBalloonTaskRequest =
+                androidx.work.OneTimeWorkRequest.Builder(StorageBalloonWorker::class.java)
+                    .setInitialDelay(1, TimeUnit.SECONDS)
+                    .build()
+            androidx.work.WorkManager.getInstance(ctx)
+                .enqueueUniqueWork(
+                    "storageBalloonTask",
+                    androidx.work.ExistingWorkPolicy.REPLACE,
+                    storageBalloonTaskRequest,
+                )
+        }
+    }
+}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/TerminalTabFragment.kt b/android/TerminalApp/java/com/android/virtualization/terminal/TerminalTabFragment.kt
index 7e78235..a0c6e4e 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/TerminalTabFragment.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/TerminalTabFragment.kt
@@ -31,8 +31,9 @@
 import android.webkit.WebSettings
 import android.webkit.WebView
 import android.webkit.WebViewClient
+import android.widget.TextView
 import androidx.fragment.app.Fragment
-import androidx.lifecycle.ViewModelProvider
+import androidx.fragment.app.activityViewModels
 import com.android.system.virtualmachine.flags.Flags.terminalGuiSupport
 import com.android.virtualization.terminal.CertificateUtils.createOrGetKey
 import com.android.virtualization.terminal.CertificateUtils.writeCertificateToFile
@@ -45,7 +46,7 @@
     private lateinit var id: String
     private var certificates: Array<X509Certificate>? = null
     private var privateKey: PrivateKey? = null
-    private lateinit var terminalViewModel: TerminalViewModel
+    private val terminalViewModel: TerminalViewModel by activityViewModels()
 
     override fun onCreateView(
         inflater: LayoutInflater,
@@ -59,7 +60,6 @@
 
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
         super.onViewCreated(view, savedInstanceState)
-        terminalViewModel = ViewModelProvider(this)[TerminalViewModel::class.java]
         terminalView = view.findViewById(R.id.webview)
         bootProgressView = view.findViewById(R.id.boot_progress)
         initializeWebView()
@@ -79,19 +79,46 @@
         terminalView.saveState(outState)
     }
 
+    override fun onResume() {
+        super.onResume()
+        updateFocus()
+    }
+
     private fun initializeWebView() {
         terminalView.settings.databaseEnabled = true
         terminalView.settings.domStorageEnabled = true
         terminalView.settings.javaScriptEnabled = true
         terminalView.settings.cacheMode = WebSettings.LOAD_DEFAULT
 
-        terminalView.webChromeClient = WebChromeClient()
+        terminalView.webChromeClient = TerminalWebChromeClient()
         terminalView.webViewClient = TerminalWebViewClient()
 
         (activity as MainActivity).modifierKeysController.addTerminalView(terminalView)
         terminalViewModel.terminalViews.add(terminalView)
     }
 
+    private inner class TerminalWebChromeClient : WebChromeClient() {
+        override fun onReceivedTitle(view: WebView?, title: String?) {
+            super.onReceivedTitle(view, title)
+            title?.let { originalTitle ->
+                val ttydSuffix = " | login -f droid (localhost)"
+                val displayedTitle =
+                    if (originalTitle.endsWith(ttydSuffix)) {
+                        // When the session is created. The format of the title will be
+                        // 'droid@localhost: ~ | login -f droid (localhost)'.
+                        originalTitle.dropLast(ttydSuffix.length)
+                    } else {
+                        originalTitle
+                    }
+
+                terminalViewModel.terminalTabs[id]
+                    ?.customView
+                    ?.findViewById<TextView>(R.id.tab_title)
+                    ?.text = displayedTitle
+            }
+        }
+    }
+
     private inner class TerminalWebViewClient : WebViewClient() {
         private var loadFailed = false
         private var requestId: Long = 0
@@ -148,6 +175,7 @@
                             terminalView.visibility = View.VISIBLE
                             terminalView.mapTouchToMouseEvent()
                             updateMainActivity()
+                            updateFocus()
                         }
                     }
                 },
@@ -189,6 +217,12 @@
         certificates = arrayOf<X509Certificate>(pke.certificate as X509Certificate)
     }
 
+    private fun updateFocus() {
+        if (terminalViewModel.selectedTabViewId == id) {
+            terminalView.requestFocus()
+        }
+    }
+
     companion object {
         const val TAG: String = "VmTerminalApp"
     }
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/TerminalViewModel.kt b/android/TerminalApp/java/com/android/virtualization/terminal/TerminalViewModel.kt
index 4a69f75..dd40143 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/TerminalViewModel.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/TerminalViewModel.kt
@@ -16,7 +16,10 @@
 package com.android.virtualization.terminal
 
 import androidx.lifecycle.ViewModel
+import com.google.android.material.tabs.TabLayout.Tab
 
 class TerminalViewModel : ViewModel() {
     val terminalViews: MutableSet<TerminalView> = mutableSetOf()
+    var selectedTabViewId: String? = null
+    val terminalTabs: MutableMap<String, Tab> = mutableMapOf()
 }
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt b/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt
index 198cf6b..067d540 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt
@@ -15,7 +15,6 @@
  */
 package com.android.virtualization.terminal
 
-import android.app.ForegroundServiceStartNotAllowedException
 import android.app.Notification
 import android.app.NotificationManager
 import android.app.PendingIntent
@@ -39,10 +38,10 @@
 import android.system.virtualmachine.VirtualMachineException
 import android.util.Log
 import android.widget.Toast
-import com.android.system.virtualmachine.flags.Flags.terminalGuiSupport
+import androidx.annotation.WorkerThread
+import com.android.system.virtualmachine.flags.Flags
+import com.android.virtualization.terminal.MainActivity.Companion.PREFIX
 import com.android.virtualization.terminal.MainActivity.Companion.TAG
-import com.android.virtualization.terminal.Runner.Companion.create
-import com.android.virtualization.terminal.VmLauncherService.VmLauncherServiceCallback
 import io.grpc.Grpc
 import io.grpc.InsecureServerCredentials
 import io.grpc.Metadata
@@ -55,7 +54,6 @@
 import java.io.File
 import java.io.FileOutputStream
 import java.io.IOException
-import java.lang.RuntimeException
 import java.net.InetSocketAddress
 import java.net.SocketAddress
 import java.nio.file.Files
@@ -65,7 +63,11 @@
 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
     private var virtualMachine: VirtualMachine? = null
@@ -90,81 +92,94 @@
 
     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)
     }
 
     override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
         val resultReceiver =
-            intent.getParcelableExtra<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.
+        when (intent.action) {
+            ACTION_START_VM -> {
+                val notification =
+                    intent.getParcelableExtra<Notification>(
+                        EXTRA_NOTIFICATION,
+                        Notification::class.java,
+                    )!!
+
+                val displayInfo =
+                    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())
+
+                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 -> {
+                Log.e(TAG, "Unknown command " + intent.action)
                 stopSelf()
             }
-            return START_NOT_STICKY
-        }
-        if (virtualMachine != null) {
-            Log.d(TAG, "VM instance is already started")
-            return START_NOT_STICKY
         }
 
+        return START_NOT_STICKY
+    }
+
+    @WorkerThread
+    private fun doStart(
+        notification: Notification,
+        displayInfo: DisplayInfo,
+        diskSize: Long,
+        resultReceiver: ResultReceiver,
+    ) {
         val image = InstalledImage.getDefault(this)
         val json = ConfigJson.from(this, image.configPath)
         val configBuilder = json.toConfigBuilder(this)
         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()
         )
-        if (overrideConfigIfNecessary(customImageConfigBuilder, displaySize)) {
+        if (overrideConfigIfNecessary(customImageConfigBuilder, displayInfo)) {
             configBuilder.setCustomImageConfig(customImageConfigBuilder.build())
         }
         val config = configBuilder.build()
 
         runner =
             try {
-                create(this, config)
+                Runner.create(this, config)
             } catch (e: VirtualMachineException) {
                 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 ->
             mbc.stop()
-            resultReceiver?.send(if (success) RESULT_STOP else RESULT_ERROR, null)
+            resultReceiver.send(if (success) RESULT_STOP else RESULT_ERROR, null)
             stopSelf()
         }
-        val logDir = getFileStreamPath(virtualMachine!!.name + ".log").toPath()
-        Logger.setup(virtualMachine!!, logDir, executorService)
+        val logDir = getFileStreamPath(virtualMachine.name + ".log").toPath()
+        Logger.setup(virtualMachine, logDir, bgThreads)
 
-        val notification =
-            intent.getParcelableExtra<Notification?>(EXTRA_NOTIFICATION, Notification::class.java)
-
-        startForeground(this.hashCode(), notification)
-
-        resultReceiver!!.send(RESULT_START, null)
+        resultReceiver.send(RESULT_START, null)
 
         portNotifier = PortNotifier(this)
 
@@ -176,22 +191,20 @@
                     val bundle = Bundle()
                     bundle.putString(KEY_TERMINAL_IPADDRESS, ipAddress)
                     bundle.putInt(KEY_TERMINAL_PORT, port)
-                    resultReceiver!!.send(RESULT_TERMINAL_AVAIL, bundle)
+                    resultReceiver.send(RESULT_TERMINAL_AVAIL, bundle)
                     startDebianServer(ipAddress)
                 },
-                executorService,
+                bgThreads,
             )
             .exceptionallyAsync(
                 { e ->
                     Log.e(TAG, "Failed to start VM", e)
-                    resultReceiver!!.send(RESULT_ERROR, null)
+                    resultReceiver.send(RESULT_ERROR, null)
                     stopSelf()
                     null
                 },
-                executorService,
+                bgThreads,
             )
-
-        return START_NOT_STICKY
     }
 
     private fun getTerminalServiceInfo(): CompletableFuture<NsdServiceInfo> {
@@ -295,7 +308,7 @@
 
         // Set the initial display size
         // TODO(jeongik): set up the display size on demand
-        if (terminalGuiSupport() && displayInfo != null) {
+        if (Flags.terminalGuiSupport() && displayInfo != null) {
             builder
                 .setDisplayConfig(
                     VirtualMachineCustomImageConfig.DisplayConfig.Builder()
@@ -357,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")
@@ -370,39 +383,59 @@
                 }
             }
         )
+
+        if (Flags.terminalStorageBalloon()) {
+            StorageBalloonWorker.start(this, debianService!!)
+        }
     }
 
-    override fun onDestroy() {
-        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)
-                }
+    @WorkerThread
+    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)
+                stopSelf()
             }
-            virtualMachine = null
+            runner = null
+        } else {
+            // If there is no Debian service or it fails to shutdown, just stop the service.
+            runner?.vm?.stop()
+            stopSelf()
         }
-        executorService.shutdownNow()
-        super.onDestroy()
     }
 
     private fun stopDebianServer() {
         debianService?.killForwarderHost()
+        debianService?.closeStorageBalloonRequestQueue()
         server?.shutdown()
     }
 
+    override fun onDestroy() {
+        mainWorkerThread.submit({
+            if (runner?.vm?.getStatus() == VirtualMachine.STATUS_RUNNING) {
+                doShutdown(null)
+            }
+        })
+        portNotifier?.stop()
+        getSystemService<NotificationManager?>(NotificationManager::class.java).cancelAll()
+        stopDebianServer()
+        bgThreads.shutdownNow()
+        mainWorkerThread.shutdown()
+        super.onDestroy()
+    }
+
     companion object {
-        private const val EXTRA_NOTIFICATION = "EXTRA_NOTIFICATION"
-        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 ACTION_START_VM: String = PREFIX + "ACTION_START_VM"
+        private const val EXTRA_NOTIFICATION = PREFIX + "EXTRA_NOTIFICATION"
+        private const val EXTRA_DISPLAY_INFO = PREFIX + "EXTRA_DISPLAY_INFO"
+        private const val EXTRA_DISK_SIZE = PREFIX + "EXTRA_DISK_SIZE"
+
+        private const val ACTION_SHUTDOWN_VM: String = PREFIX + "ACTION_SHUTDOWN_VM"
 
         private const val RESULT_START = 0
         private const val RESULT_STOP = 1
@@ -425,24 +458,11 @@
                 }
             }()
 
-        private fun getMyIntent(context: Context): Intent {
-            return Intent(context.getApplicationContext(), VmLauncherService::class.java)
-        }
-
-        fun run(
-            context: Context,
-            callback: VmLauncherServiceCallback?,
-            notification: Notification?,
-            displayInfo: DisplayInfo,
-            diskSize: Long?,
-        ): Result<Unit> {
-            val i = getMyIntent(context)
-            val resultReceiver: ResultReceiver =
+        private fun prepareIntent(context: Context, callback: VmLauncherServiceCallback): Intent {
+            val intent = Intent(context.getApplicationContext(), VmLauncherService::class.java)
+            val resultReceiver =
                 object : ResultReceiver(Handler(Looper.myLooper()!!)) {
                     override fun onReceiveResult(resultCode: Int, resultData: Bundle?) {
-                        if (callback == null) {
-                            return
-                        }
                         when (resultCode) {
                             RESULT_START -> callback.onVmStart()
                             RESULT_TERMINAL_AVAIL -> {
@@ -452,46 +472,42 @@
                             }
                             RESULT_STOP -> callback.onVmStop()
                             RESULT_ERROR -> callback.onVmError()
+                            else -> Log.e(TAG, "unknown result code: " + resultCode)
                         }
                     }
                 }
-            i.putExtra(Intent.EXTRA_RESULT_RECEIVER, getResultReceiverForIntent(resultReceiver))
+
+            val parcel = Parcel.obtain()
+            resultReceiver.writeToParcel(parcel, 0)
+            parcel.setDataPosition(0)
+            intent.putExtra(
+                Intent.EXTRA_RESULT_RECEIVER,
+                ResultReceiver.CREATOR.createFromParcel(parcel).also { parcel.recycle() },
+            )
+            return intent
+        }
+
+        fun getIntentForStart(
+            context: Context,
+            callback: VmLauncherServiceCallback,
+            notification: Notification?,
+            displayInfo: DisplayInfo,
+            diskSize: Long?,
+        ): Intent {
+            val i = prepareIntent(context, callback)
+            i.setAction(ACTION_START_VM)
             i.putExtra(EXTRA_NOTIFICATION, notification)
             i.putExtra(EXTRA_DISPLAY_INFO, displayInfo)
             if (diskSize != null) {
                 i.putExtra(EXTRA_DISK_SIZE, diskSize)
             }
-            return try {
-                context.startForegroundService(i)
-                Result.success(Unit)
-            } catch (e: ForegroundServiceStartNotAllowedException) {
-                Result.failure<Unit>(e)
-            }
+            return i
         }
 
-        private fun getResultReceiverForIntent(r: ResultReceiver): ResultReceiver {
-            val parcel = Parcel.obtain()
-            r.writeToParcel(parcel, 0)
-            parcel.setDataPosition(0)
-            return ResultReceiver.CREATOR.createFromParcel(parcel).also { parcel.recycle() }
-        }
-
-        fun stop(context: Context, callback: VmLauncherServiceCallback?) {
-            val i = getMyIntent(context)
+        fun getIntentForShutdown(context: Context, callback: VmLauncherServiceCallback): Intent {
+            val i = prepareIntent(context, callback)
             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)
+            return i
         }
     }
 }
diff --git a/android/TerminalApp/res/layout/tabitem_terminal.xml b/android/TerminalApp/res/layout/tabitem_terminal.xml
index 92e3802..9eba163 100644
--- a/android/TerminalApp/res/layout/tabitem_terminal.xml
+++ b/android/TerminalApp/res/layout/tabitem_terminal.xml
@@ -25,7 +25,7 @@
       android:layout_alignParentStart="true"
       android:layout_alignParentTop="true"
       android:layout_toStartOf="@id/tab_close_button"
-      android:gravity="center"
+      android:gravity="center_vertical"
       android:padding="8dp"
       android:text="@string/tab_default_title"/>
 
diff --git a/android/virtmgr/src/aidl.rs b/android/virtmgr/src/aidl.rs
index 1c4c2eb..1e756eb 100644
--- a/android/virtmgr/src/aidl.rs
+++ b/android/virtmgr/src/aidl.rs
@@ -597,9 +597,10 @@
         config: &VirtualMachineConfig,
     ) -> binder::Result<(VmContext, Cid, PathBuf)> {
         const NUM_ATTEMPTS: usize = 5;
+        let name = get_name(config);
 
         for _ in 0..NUM_ATTEMPTS {
-            let vm_context = GLOBAL_SERVICE.allocateGlobalVmContext(requester_debug_pid)?;
+            let vm_context = GLOBAL_SERVICE.allocateGlobalVmContext(name, requester_debug_pid)?;
             let cid = vm_context.getCid()? as Cid;
             let temp_dir: PathBuf = vm_context.getTemporaryDirectory()?.into();
 
@@ -1053,6 +1054,14 @@
     }
 }
 
+/// Returns the name of the config
+fn get_name(config: &VirtualMachineConfig) -> &str {
+    match config {
+        VirtualMachineConfig::AppConfig(config) => &config.name,
+        VirtualMachineConfig::RawConfig(config) => &config.name,
+    }
+}
+
 fn extract_vendor_hashtree_digest(config: &VirtualMachineConfig) -> Result<Option<Vec<u8>>> {
     let VirtualMachineConfig::AppConfig(config) = config else {
         return Ok(None);
diff --git a/android/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineDebugInfo.aidl b/android/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineDebugInfo.aidl
index 9f033b1..eb71028 100644
--- a/android/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineDebugInfo.aidl
+++ b/android/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineDebugInfo.aidl
@@ -19,6 +19,9 @@
 
 /** Information about a running VM, for debug purposes only. */
 parcelable VirtualMachineDebugInfo {
+    /** Name of the VM. */
+    String name;
+
     /** The CID assigned to the VM. */
     int cid;
 
diff --git a/android/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVfioHandler.aidl b/android/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVfioHandler.aidl
index 2cf4efd..4ded2a9 100644
--- a/android/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVfioHandler.aidl
+++ b/android/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVfioHandler.aidl
@@ -16,7 +16,6 @@
 package android.system.virtualizationservice_internal;
 
 import android.system.virtualizationservice.AssignableDevice;
-import android.system.virtualizationservice.VirtualMachineDebugInfo;
 import android.system.virtualizationservice_internal.AtomVmBooted;
 import android.system.virtualizationservice_internal.AtomVmCreationRequested;
 import android.system.virtualizationservice_internal.AtomVmExited;
diff --git a/android/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVirtualizationServiceInternal.aidl b/android/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVirtualizationServiceInternal.aidl
index 4f549cb..3d4a813 100644
--- a/android/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVirtualizationServiceInternal.aidl
+++ b/android/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVirtualizationServiceInternal.aidl
@@ -39,7 +39,7 @@
      * The resources will not be recycled as long as there is a strong reference
      * to the returned object.
      */
-    IGlobalVmContext allocateGlobalVmContext(int requesterDebugPid);
+    IGlobalVmContext allocateGlobalVmContext(String name, int requesterDebugPid);
 
     /** Forwards a VmBooted atom to statsd. */
     void atomVmBooted(in AtomVmBooted atom);
diff --git a/android/virtualizationservice/src/aidl.rs b/android/virtualizationservice/src/aidl.rs
index 62cede8..1646117 100644
--- a/android/virtualizationservice/src/aidl.rs
+++ b/android/virtualizationservice/src/aidl.rs
@@ -273,6 +273,7 @@
 
     fn allocateGlobalVmContext(
         &self,
+        name: &str,
         requester_debug_pid: i32,
     ) -> binder::Result<Strong<dyn IGlobalVmContext>> {
         check_manage_access()?;
@@ -281,7 +282,7 @@
         let requester_debug_pid = requester_debug_pid as pid_t;
         let state = &mut *self.state.lock().unwrap();
         state
-            .allocate_vm_context(requester_uid, requester_debug_pid)
+            .allocate_vm_context(name, requester_uid, requester_debug_pid)
             .or_binder_exception(ExceptionCode::ILLEGAL_STATE)
     }
 
@@ -311,6 +312,7 @@
             .map(|vm| {
                 let vm = vm.lock().unwrap();
                 VirtualMachineDebugInfo {
+                    name: vm.name.clone(),
                     cid: vm.cid as i32,
                     temporaryDirectory: vm.get_temp_dir().to_string_lossy().to_string(),
                     requesterUid: vm.requester_uid as i32,
@@ -665,6 +667,8 @@
 
 #[derive(Debug, Default)]
 struct GlobalVmInstance {
+    /// Name of the VM
+    name: String,
     /// The unique CID assigned to the VM for vsock communication.
     cid: Cid,
     /// UID of the client who requested this VM instance.
@@ -760,6 +764,7 @@
 
     fn allocate_vm_context(
         &mut self,
+        name: &str,
         requester_uid: uid_t,
         requester_debug_pid: pid_t,
     ) -> Result<Strong<dyn IGlobalVmContext>> {
@@ -768,6 +773,7 @@
 
         let cid = self.get_next_available_cid()?;
         let instance = Arc::new(Mutex::new(GlobalVmInstance {
+            name: name.to_owned(),
             cid,
             requester_uid,
             requester_debug_pid,
diff --git a/build/avf_flags.aconfig b/build/avf_flags.aconfig
index 921c374..571c359 100644
--- a/build/avf_flags.aconfig
+++ b/build/avf_flags.aconfig
@@ -16,4 +16,12 @@
   namespace: "virtualization"
   description: "Flag for GUI support in terminal"
   bug: "386296118"
+}
+
+flag {
+  name: "terminal_storage_balloon"
+  is_exported: true
+  namespace: "virtualization"
+  description: "Flag for storage ballooning support in terminal"
+  bug: "382174138"
 }
\ No newline at end of file
diff --git a/build/debian/build.sh b/build/debian/build.sh
index 9c4d4b1..8c1345c 100755
--- a/build/debian/build.sh
+++ b/build/debian/build.sh
@@ -204,6 +204,7 @@
 	build_rust_as_deb forwarder_guest
 	build_rust_as_deb forwarder_guest_launcher
 	build_rust_as_deb shutdown_runner
+	build_rust_as_deb storage_balloon_agent
 }
 
 package_custom_kernel() {
diff --git a/build/debian/fai_config/package_config/AVF b/build/debian/fai_config/package_config/AVF
index 3aa8ab0..f1ee065 100644
--- a/build/debian/fai_config/package_config/AVF
+++ b/build/debian/fai_config/package_config/AVF
@@ -8,6 +8,7 @@
 forwarder-guest
 forwarder-guest-launcher
 shutdown-runner
+storage-balloon-agent
 weston
 xwayland
 mesa-vulkan-drivers
diff --git a/build/debian/fai_config/scripts/AVF/20-useradd b/build/debian/fai_config/scripts/AVF/20-useradd
index b92648a..2289a2a 100755
--- a/build/debian/fai_config/scripts/AVF/20-useradd
+++ b/build/debian/fai_config/scripts/AVF/20-useradd
@@ -2,3 +2,7 @@
 
 $ROOTCMD useradd -m -u 1000 -N -G sudo,video,render -s /usr/bin/bash droid
 $ROOTCMD echo 'droid ALL=(ALL) NOPASSWD:ALL' >> $target/etc/sudoers
+$ROOTCMD cat >> $target/home/droid/.bashrc <<EOF
+# Show title of current running command
+trap 'echo -ne "\e]0;\$BASH_COMMAND\007"' DEBUG
+EOF
diff --git a/guest/microdroid_manager/microdroid_manager.rc b/guest/microdroid_manager/microdroid_manager.rc
index 9fa8a9d..48cc6d7 100644
--- a/guest/microdroid_manager/microdroid_manager.rc
+++ b/guest/microdroid_manager/microdroid_manager.rc
@@ -8,6 +8,7 @@
     # CAP_SYS_BOOT is required to exec kexecload from microdroid_manager
     # CAP_SETPCAP is required to allow microdroid_manager to drop capabilities
     #   before executing the payload
-    capabilities AUDIT_CONTROL SYS_ADMIN SYS_BOOT SETPCAP SETUID SETGID
+    # CAP_SYS_NICE is required for microdroid_manager to adjust priority of the payload
+    capabilities AUDIT_CONTROL SYS_ADMIN SYS_BOOT SETPCAP SETUID SETGID SYS_NICE
     user root
     socket vm_payload_service stream 0666 system system
diff --git a/guest/microdroid_manager/src/main.rs b/guest/microdroid_manager/src/main.rs
index 4537834..a95bcb2 100644
--- a/guest/microdroid_manager/src/main.rs
+++ b/guest/microdroid_manager/src/main.rs
@@ -710,7 +710,21 @@
     info!("notifying payload started");
     service.notifyPayloadStarted()?;
 
-    let exit_status = command.spawn()?.wait()?;
+    let mut payload_process = command.spawn().context("failed to spawn payload process")?;
+    info!("payload pid = {:?}", payload_process.id());
+
+    // SAFETY: setpriority doesn't take any pointers
+    unsafe {
+        let ret = libc::setpriority(libc::PRIO_PROCESS, payload_process.id(), -20);
+        if ret != 0 {
+            error!(
+                "failed to adjust priority of the payload: {:#?}",
+                std::io::Error::last_os_error()
+            );
+        }
+    }
+
+    let exit_status = payload_process.wait()?;
     match exit_status.code() {
         Some(exit_code) => Ok(exit_code),
         None => Err(match exit_status.signal() {
diff --git a/guest/storage_balloon_agent/.cargo/config.toml b/guest/storage_balloon_agent/.cargo/config.toml
new file mode 100644
index 0000000..a451cda
--- /dev/null
+++ b/guest/storage_balloon_agent/.cargo/config.toml
@@ -0,0 +1,6 @@
+[target.aarch64-unknown-linux-gnu]
+linker = "aarch64-linux-gnu-gcc"
+rustflags = ["-C", "target-feature=+crt-static"]
+
+[target.x86_64-unknown-linux-gnu]
+rustflags = ["-C", "target-feature=+crt-static"]
diff --git a/guest/storage_balloon_agent/.gitignore b/guest/storage_balloon_agent/.gitignore
new file mode 100644
index 0000000..ea8c4bf
--- /dev/null
+++ b/guest/storage_balloon_agent/.gitignore
@@ -0,0 +1 @@
+/target
diff --git a/guest/storage_balloon_agent/Cargo.toml b/guest/storage_balloon_agent/Cargo.toml
new file mode 100644
index 0000000..ce0e5d7
--- /dev/null
+++ b/guest/storage_balloon_agent/Cargo.toml
@@ -0,0 +1,26 @@
+[package]
+name = "storage_balloon_agent"
+version = "0.1.0"
+edition = "2021"
+license = "Apache-2.0"
+
+[dependencies]
+anyhow = "1.0.94"
+clap = { version = "4.5.20", features = ["derive"] }
+env_logger = "0.10.2"
+log = "0.4.22"
+netdev = "0.31.0"
+nix = { version = "0.28.0", features = ["fs"] }
+prost = "0.13.3"
+tokio = { version = "1.40.0", features = ["rt-multi-thread"] }
+tonic = "0.12.3"
+
+[build-dependencies]
+tonic-build = "0.12.3"
+
+[package.metadata.deb]
+maintainer = "ferrochrome-dev@google.com"
+copyright = "2025, The Android Open Source Project"
+depends = "$auto"
+maintainer-scripts = "debian/"
+systemd-units = { }
diff --git a/guest/storage_balloon_agent/build.rs b/guest/storage_balloon_agent/build.rs
new file mode 100644
index 0000000..e3939d4
--- /dev/null
+++ b/guest/storage_balloon_agent/build.rs
@@ -0,0 +1,7 @@
+fn main() -> Result<(), Box<dyn std::error::Error>> {
+    let proto_file = "../../libs/debian_service/proto/DebianService.proto";
+
+    tonic_build::compile_protos(proto_file).unwrap();
+
+    Ok(())
+}
diff --git a/guest/storage_balloon_agent/debian/service b/guest/storage_balloon_agent/debian/service
new file mode 100644
index 0000000..0e9b03a
--- /dev/null
+++ b/guest/storage_balloon_agent/debian/service
@@ -0,0 +1,17 @@
+[Unit]
+After=syslog.target
+After=network.target
+After=virtiofs_internal.service
+
+[Service]
+ExecStart=/usr/bin/bash -c '/usr/bin/storage_balloon_agent --grpc_port_file /mnt/internal/debian_service_port'
+Type=simple
+Restart=on-failure
+RestartSec=1
+User=root
+Group=root
+StandardOutput=journal
+StandardError=journal
+
+[Install]
+WantedBy=multi-user.target
diff --git a/guest/storage_balloon_agent/rustfmt.toml b/guest/storage_balloon_agent/rustfmt.toml
new file mode 120000
index 0000000..be3dbe2
--- /dev/null
+++ b/guest/storage_balloon_agent/rustfmt.toml
@@ -0,0 +1 @@
+../../../../../build/soong/scripts/rustfmt.toml
\ No newline at end of file
diff --git a/guest/storage_balloon_agent/src/main.rs b/guest/storage_balloon_agent/src/main.rs
new file mode 100644
index 0000000..817b337
--- /dev/null
+++ b/guest/storage_balloon_agent/src/main.rs
@@ -0,0 +1,141 @@
+// 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.
+
+//! gRPC daemon for the storage ballooning feature.
+
+use anyhow::anyhow;
+use anyhow::Context;
+use anyhow::Result;
+use api::debian_service_client::DebianServiceClient;
+use api::StorageBalloonQueueOpeningRequest;
+use api::StorageBalloonRequestItem;
+use clap::Parser;
+use log::debug;
+use log::error;
+use log::info;
+use nix::sys::statvfs::statvfs;
+pub mod api {
+    tonic::include_proto!("com.android.virtualization.terminal.proto");
+}
+
+#[derive(Parser)]
+/// Flags for running command
+pub struct Args {
+    /// IP address
+    #[arg(long)]
+    addr: Option<String>,
+
+    /// path to a file where grpc port number is written
+    #[arg(long)]
+    #[arg(alias = "grpc_port_file")]
+    grpc_port_file: String,
+}
+
+// Calculates how many blocks to be reserved.
+fn calculate_clusters_count(guest_available_bytes: u64) -> Result<u64> {
+    let stat = statvfs("/").context("failed to get statvfs")?;
+    let fr_size = stat.fragment_size() as u64;
+
+    if fr_size == 0 {
+        return Err(anyhow::anyhow!("fragment size is zero, fr_size: {}", fr_size));
+    }
+
+    let total = fr_size.checked_mul(stat.blocks() as u64).context(format!(
+        "overflow in total size calculation, fr_size: {}, blocks: {}",
+        fr_size,
+        stat.blocks()
+    ))?;
+
+    let free = fr_size.checked_mul(stat.blocks_available() as u64).context(format!(
+        "overflow in free size calculation, fr_size: {}, blocks_available: {}",
+        fr_size,
+        stat.blocks_available()
+    ))?;
+
+    let used = total
+        .checked_sub(free)
+        .context(format!("underflow in used size calculation (free > total), which should not happen, total: {}, free: {}", total, free))?;
+
+    let avail = std::cmp::min(free, guest_available_bytes);
+    let balloon_size_bytes = free - avail;
+
+    let reserved_clusters_count = balloon_size_bytes.div_ceil(fr_size);
+
+    debug!("total: {total}, free: {free}, used: {used}, avail: {avail}, balloon: {balloon_size_bytes}, clusters_count: {reserved_clusters_count}");
+
+    Ok(reserved_clusters_count)
+}
+
+fn set_reserved_clusters(clusters_count: u64) -> anyhow::Result<()> {
+    const ROOTFS_DEVICE_NAME: &str = "vda1";
+    std::fs::write(
+        format!("/sys/fs/ext4/{ROOTFS_DEVICE_NAME}/reserved_clusters"),
+        clusters_count.to_string(),
+    )
+    .context("failed to write reserved_clusters")?;
+    Ok(())
+}
+
+#[tokio::main]
+async fn main() -> Result<(), Box<dyn std::error::Error>> {
+    env_logger::builder().filter_level(log::LevelFilter::Debug).init();
+
+    let args = Args::parse();
+    let gateway_ip_addr = netdev::get_default_gateway()?.ipv4[0];
+    let addr = args.addr.unwrap_or_else(|| gateway_ip_addr.to_string());
+
+    // Wait for `grpc_port_file` becomes available.
+    const GRPC_PORT_MAX_RETRY_COUNT: u32 = 10;
+    for _ in 0..GRPC_PORT_MAX_RETRY_COUNT {
+        if std::path::Path::new(&args.grpc_port_file).exists() {
+            break;
+        }
+        debug!("{} does not exist. Wait 1 second", args.grpc_port_file);
+        tokio::time::sleep(std::time::Duration::from_secs(1)).await;
+    }
+    let grpc_port = std::fs::read_to_string(&args.grpc_port_file)?.trim().to_string();
+    let server_addr = format!("http://{}:{}", addr, grpc_port);
+
+    info!("connect to grpc server {}", server_addr);
+    let mut client = DebianServiceClient::connect(server_addr)
+        .await
+        .map_err(|e| anyhow!("failed to connect to grpc server: {:#}", e))?;
+    info!("connection established");
+
+    let mut res_stream = client
+        .open_storage_balloon_request_queue(tonic::Request::new(
+            StorageBalloonQueueOpeningRequest {},
+        ))
+        .await
+        .map_err(|e| anyhow!("failed to open storage balloon queue: {:#}", e))?
+        .into_inner();
+
+    while let Some(StorageBalloonRequestItem { available_bytes }) =
+        res_stream.message().await.map_err(|e| anyhow!("failed to receive message: {:#}", e))?
+    {
+        let clusters_count = match calculate_clusters_count(available_bytes) {
+            Ok(c) => c,
+            Err(e) => {
+                error!("failed to calculate cluster size to be reserved: {:#}", e);
+                continue;
+            }
+        };
+
+        if let Err(e) = set_reserved_clusters(clusters_count) {
+            error!("failed to set storage balloon size: {}", e);
+        }
+    }
+
+    Ok(())
+}
diff --git a/guest/trusty/test_vm/AndroidTest.xml b/guest/trusty/test_vm/AndroidTest.xml
index 925b43c..43d9ef8 100644
--- a/guest/trusty/test_vm/AndroidTest.xml
+++ b/guest/trusty/test_vm/AndroidTest.xml
@@ -15,10 +15,10 @@
   limitations under the License.
   -->
     <configuration description="Runs {MODULE}">
-    <!-- object type="module_controller" class="com.android.tradefed.testtype.suite.module.CommandSuccessModuleController" -->
+    <object type="module_controller" class="com.android.tradefed.testtype.suite.module.CommandSuccessModuleController">
         <!--Skip the test when trusty VM is not enabled. -->
-        <!--option name="run-command" value="getprop trusty.test_vm.nonsecure_vm_ready | grep 1" /-->
-    <!--/object-->
+        <option name="run-command" value="getprop trusty.security_vm.enabled | grep 1" />
+    </object>
     <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer" />
     <!-- Target Preparers - Run Shell Commands -->
     <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
@@ -38,13 +38,19 @@
         <option name="run-command" value="start storageproxyd_test_vm" />
         <option name="teardown-command" value="stop storageproxyd_test_vm" />
         <option name="teardown-command" value="killall storageproxyd_test_vm || true" />
+        <!--option name="teardown-command" value="rm -rf /data/local/trusty_test_vm"/-->
     </target_preparer>
     <test class="com.android.tradefed.testtype.binary.ExecutableTargetTest" >
         <option name="parse-gtest" value="true" />
         <option name="abort-if-device-lost" value="true"/>
         <option name="abort-if-root-lost" value="true" />
         <option name="per-binary-timeout" value="10m" />
+        <option name="test-command-line" key="com.android.trusty.rust.authmgr_be_lib.test" value="/data/local/tmp/trusty_test_vm/trusty-ut-ctrl.sh com.android.trusty.rust.authmgr_be_lib.test"/>
         <option name="test-command-line" key="com.android.trusty.rust.hwcryptokey_test.test" value="/data/local/tmp/trusty_test_vm/trusty-ut-ctrl.sh com.android.trusty.rust.hwcryptokey_test.test"/>
         <option name="test-command-line" key="com.android.trusty.rust.storage_unittest_aidl.test" value="/data/local/tmp/trusty_test_vm/trusty-ut-ctrl.sh com.android.trusty.rust.storage_unittest_aidl.test"/>
     </test>
+    <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
+        <option name="directory-keys" value="/data/local/tmp/trusty_test_vm/logs" />
+        <option name="clean-up" value="false"/>
+    </metrics_collector>
     </configuration>
diff --git a/guest/trusty/test_vm/README.md b/guest/trusty/test_vm/README.md
index 71368b5..81382c5 100644
--- a/guest/trusty/test_vm/README.md
+++ b/guest/trusty/test_vm/README.md
@@ -11,3 +11,16 @@
 The Trusty test_vm also includes the VINTF test which allows to check the vendor
 support of the Trusted HALs (version and API hash), against the expected
 compatibility matrix for a given Android Dessert Release.
+
+### instructions
+
+`atest -s <device-serial-port> VtsSeeHalTargetTest
+
+### test_vm console
+
+The test_vm console can be retrieved from `/data/local/tmp/trusty_test_vm/logs/console.log`.
+The script `trusty-vm-laucher.sh` uses `/apex/com.android.virt/bin/vm run` with the option
+`--console` to store the console log.
+
+This log can be consulted when the tests are running and will be uploaded
+by the Tradefed FilePullerLogCollector runner (see AndroidTest.xml).
diff --git a/guest/trusty/test_vm/TEST_MAPPING b/guest/trusty/test_vm/TEST_MAPPING
deleted file mode 100644
index aa9d65d..0000000
--- a/guest/trusty/test_vm/TEST_MAPPING
+++ /dev/null
@@ -1,9 +0,0 @@
-{
-  "trusty_test_vm_presubmit": [
-  ],
-  "trusty_test_vm_postsubmit": [
-    {
-        "name": "TrustyTestVM_UnitTests"
-    }
-  ]
-}
diff --git a/guest/trusty/test_vm/trusty-vm-launcher.sh b/guest/trusty/test_vm/trusty-vm-launcher.sh
index cb8661f..079a66a 100755
--- a/guest/trusty/test_vm/trusty-vm-launcher.sh
+++ b/guest/trusty/test_vm/trusty-vm-launcher.sh
@@ -14,4 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-/apex/com.android.virt/bin/vm run /data/local/tmp/trusty_test_vm/trusty-test_vm-config.json
+mkdir -p /data/local/tmp/trusty_test_vm/logs || true
+/apex/com.android.virt/bin/vm run \
+   --console /data/local/tmp/trusty_test_vm/logs/console.log \
+   /data/local/tmp/trusty_test_vm/trusty-test_vm-config.json
diff --git a/guest/trusty/test_vm_os/AndroidTest.xml b/guest/trusty/test_vm_os/AndroidTest.xml
index be5c467..5adafff 100644
--- a/guest/trusty/test_vm_os/AndroidTest.xml
+++ b/guest/trusty/test_vm_os/AndroidTest.xml
@@ -15,10 +15,10 @@
   limitations under the License.
   -->
     <configuration description="Runs {MODULE}">
-    <!-- object type="module_controller" class="com.android.tradefed.testtype.suite.module.CommandSuccessModuleController" -->
+    <object type="module_controller" class="com.android.tradefed.testtype.suite.module.CommandSuccessModuleController">
         <!--Skip the test when trusty VM is not enabled. -->
-        <!--option name="run-command" value="getprop trusty.test_vm.nonsecure_vm_ready | grep 1" /-->
-    <!--/object-->
+        <option name="run-command" value="getprop trusty.security_vm.enabled | grep 1" />
+    </object>
     <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer" />
     <!-- Target Preparers - Run Shell Commands -->
     <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
@@ -38,6 +38,7 @@
         <option name="run-command" value="start storageproxyd_test_vm_os" />
         <option name="teardown-command" value="stop storageproxyd_test_vm_os" />
         <option name="teardown-command" value="killall storageproxyd_test_vm_os || true" />
+        <!--option name="teardown-command" value="rm -rf /data/local/trusty_test_vm_os"/-->
     </target_preparer>
     <test class="com.android.tradefed.testtype.binary.ExecutableTargetTest" >
         <option name="parse-gtest" value="true" />
@@ -79,4 +80,10 @@
         <option name="test-command-line" key="com.android.trusty.rust.binder_rpc_test.test" value="/data/local/tmp/trusty_test_vm_os/trusty-ut-ctrl.sh com.android.trusty.rust.binder_rpc_test.test"/>
         <option name="test-command-line" key="com.android.trusty.binder.test" value="/data/local/tmp/trusty_test_vm_os/trusty-ut-ctrl.sh com.android.trusty.binder.test"/>
     </test>
+    <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
+        <option name="directory-keys" value="/data/local/tmp/trusty_test_vm_os/logs" />
+        <option name="collect-on-run-ended-only" value="true" />
+        <option name="clean-up" value="true"/>
+        <option name="collect-on-run-ended-only" value="false" />
+    </metrics_collector>
     </configuration>
diff --git a/guest/trusty/test_vm_os/README.md b/guest/trusty/test_vm_os/README.md
index 4d65d9f..b37a4da 100644
--- a/guest/trusty/test_vm_os/README.md
+++ b/guest/trusty/test_vm_os/README.md
@@ -5,3 +5,6 @@
 - Trusty kernel OS test
 - Trusty/Binder IPC tests
 - Trusty user-space tests for service TAs (DT tree for example)
+
+
+see instructions at [test_vm/README.md](../test_vm/README.md)
diff --git a/guest/trusty/test_vm_os/TEST_MAPPING b/guest/trusty/test_vm_os/TEST_MAPPING
deleted file mode 100644
index 1506720..0000000
--- a/guest/trusty/test_vm_os/TEST_MAPPING
+++ /dev/null
@@ -1,9 +0,0 @@
-{
-  "trusty_test_vm_presubmit": [
-  ],
-  "trusty_test_vm_postsubmit": [
-    {
-        "name": "TrustyVMOS_UnitTests"
-    }
-  ]
-}
diff --git a/guest/trusty/test_vm_os/trusty-vm-launcher.sh b/guest/trusty/test_vm_os/trusty-vm-launcher.sh
index 497b188..bc256ed 100755
--- a/guest/trusty/test_vm_os/trusty-vm-launcher.sh
+++ b/guest/trusty/test_vm_os/trusty-vm-launcher.sh
@@ -14,4 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-/apex/com.android.virt/bin/vm run /data/local/tmp/trusty_test_vm_os/trusty-test_vm-config.json
+mkdir -p /data/local/tmp/trusty_test_vm_os/logs || true
+/apex/com.android.virt/bin/vm run \
+   --console /data/local/tmp/trusty_test_vm_os/logs/console.log \
+   /data/local/tmp/trusty_test_vm_os/trusty-test_vm-config.json
diff --git a/libs/debian_service/proto/DebianService.proto b/libs/debian_service/proto/DebianService.proto
index 43955fa..e52b28a 100644
--- a/libs/debian_service/proto/DebianService.proto
+++ b/libs/debian_service/proto/DebianService.proto
@@ -25,6 +25,7 @@
   rpc ReportVmActivePorts (ReportVmActivePortsRequest) returns (ReportVmActivePortsResponse) {}
   rpc OpenForwardingRequestQueue (QueueOpeningRequest) returns (stream ForwardingRequestItem) {}
   rpc OpenShutdownRequestQueue (ShutdownQueueOpeningRequest) returns (stream ShutdownRequestItem) {}
+  rpc OpenStorageBalloonRequestQueue (StorageBalloonQueueOpeningRequest) returns (stream StorageBalloonRequestItem) {}
 }
 
 message QueueOpeningRequest {
@@ -52,3 +53,9 @@
 message ShutdownQueueOpeningRequest {}
 
 message ShutdownRequestItem {}
+
+message StorageBalloonQueueOpeningRequest {}
+
+message StorageBalloonRequestItem {
+  uint64 available_bytes = 1;
+}