Snap for 13226929 from 6dc07389b34627847c54c24a0b028282ea23263c to 25Q2-release
Change-Id: I7bb77d63fa3282264bb5409df5221b4bd464d6af
diff --git a/android/TerminalApp/Android.bp b/android/TerminalApp/Android.bp
index e1e236a..c18ada4 100644
--- a/android/TerminalApp/Android.bp
+++ b/android/TerminalApp/Android.bp
@@ -14,6 +14,7 @@
// 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",
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 e81be7f..2c52283 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/DebianServiceImpl.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/DebianServiceImpl.kt
@@ -21,7 +21,6 @@
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
@@ -35,7 +34,7 @@
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()
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/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 01528bb..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,9 +38,10 @@
import android.system.virtualmachine.VirtualMachineException
import android.util.Log
import android.widget.Toast
+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 io.grpc.Grpc
import io.grpc.InsecureServerCredentials
import io.grpc.Metadata
@@ -63,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
@@ -88,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)
@@ -174,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> {
@@ -355,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")
@@ -374,23 +389,24 @@
}
}
- 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() {
@@ -399,13 +415,27 @@
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
@@ -428,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 -> {
@@ -455,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/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() {