Merge "virtmgr: check SELinux label of non-partition disk images" into main
diff --git a/android/TerminalApp/assets/js/disable_ctrl_key.js b/android/TerminalApp/assets/js/disable_ctrl_key.js
new file mode 100644
index 0000000..df261f0
--- /dev/null
+++ b/android/TerminalApp/assets/js/disable_ctrl_key.js
@@ -0,0 +1,19 @@
+/*
+ * 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.
+ */
+
+(function() {
+window.ctrl = false;
+})();
\ No newline at end of file
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/BaseActivity.kt b/android/TerminalApp/java/com/android/virtualization/terminal/BaseActivity.kt
index 70bc5e4..229cdbb 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/BaseActivity.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/BaseActivity.kt
@@ -30,10 +30,6 @@
TerminalExceptionHandler(applicationContext)
}
}
- }
-
- public override fun onResume() {
- super.onResume()
if (
applicationContext.checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) !=
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.kt b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.kt
index 0f18261..d35b106 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.kt
@@ -21,11 +21,9 @@
import android.content.Intent
import android.content.pm.ActivityInfo
import android.content.res.Configuration
-import android.graphics.Bitmap
import android.graphics.drawable.Icon
import android.graphics.fonts.FontStyle
import android.net.Uri
-import android.net.http.SslError
import android.net.nsd.NsdManager
import android.net.nsd.NsdServiceInfo
import android.os.Build
@@ -38,41 +36,32 @@
import android.util.DisplayMetrics
import android.util.Log
import android.view.KeyEvent
-import android.view.Menu
-import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.view.accessibility.AccessibilityManager
-import android.webkit.ClientCertRequest
-import android.webkit.SslErrorHandler
-import android.webkit.WebChromeClient
-import android.webkit.WebResourceError
-import android.webkit.WebResourceRequest
-import android.webkit.WebSettings
-import android.webkit.WebView
-import android.webkit.WebViewClient
+import android.widget.Button
+import android.widget.HorizontalScrollView
+import android.widget.RelativeLayout
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
+import androidx.lifecycle.ViewModelProvider
+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.CertificateUtils.createOrGetKey
-import com.android.virtualization.terminal.CertificateUtils.writeCertificateToFile
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.appbar.MaterialToolbar
+import com.google.android.material.tabs.TabLayout
+import com.google.android.material.tabs.TabLayoutMediator
import java.io.IOException
-import java.lang.Exception
import java.net.MalformedURLException
import java.net.URL
-import java.security.PrivateKey
-import java.security.cert.X509Certificate
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
@@ -80,17 +69,21 @@
BaseActivity(),
VmLauncherServiceCallback,
AccessibilityManager.AccessibilityStateChangeListener {
+ var displayMenu: Button? = null
+ var tabAddButton: Button? = null
+ val bootCompleted = ConditionVariable()
+ lateinit var modifierKeysController: ModifierKeysController
+ private lateinit var tabScrollView: HorizontalScrollView
private lateinit var executorService: ExecutorService
private lateinit var image: InstalledImage
- private var certificates: Array<X509Certificate>? = null
- private var privateKey: PrivateKey? = null
- private lateinit var terminalContainer: ViewGroup
- private lateinit var terminalView: TerminalView
private lateinit var accessibilityManager: AccessibilityManager
- private val bootCompleted = ConditionVariable()
private lateinit var manageExternalStorageActivityResultLauncher: ActivityResultLauncher<Intent>
- private lateinit var modifierKeysController: ModifierKeysController
- private var displayMenu: MenuItem? = null
+ private var ipAddress: String? = null
+ private var port: Int? = null
+ private lateinit var terminalViewModel: TerminalViewModel
+ private lateinit var viewPager: ViewPager2
+ private lateinit var tabLayout: TabLayout
+ private lateinit var terminalTabAdapter: TerminalTabAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -100,26 +93,12 @@
val launchInstaller = installIfNecessary()
- setContentView(R.layout.activity_headless)
-
- val toolbar = findViewById<MaterialToolbar>(R.id.toolbar)
- setSupportActionBar(toolbar)
- terminalView = findViewById<TerminalView>(R.id.webview)
- terminalView.getSettings().setDomStorageEnabled(true)
- terminalView.getSettings().setJavaScriptEnabled(true)
- terminalView.getSettings().setCacheMode(WebSettings.LOAD_NO_CACHE)
- terminalView.setWebChromeClient(WebChromeClient())
-
- terminalContainer = terminalView.parent as ViewGroup
-
- modifierKeysController = ModifierKeysController(this, terminalView, terminalContainer)
+ initializeUi()
accessibilityManager =
getSystemService<AccessibilityManager>(AccessibilityManager::class.java)
accessibilityManager.addAccessibilityStateChangeListener(this)
- readClientCertificate()
-
manageExternalStorageActivityResultLauncher =
registerForActivityResult<Intent, ActivityResult>(
StartActivityForResult(),
@@ -138,6 +117,69 @@
}
}
+ 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)
+ tabAddButton = findViewById<Button>(R.id.tab_add_button)
+ tabScrollView = findViewById<HorizontalScrollView>(R.id.tab_scrollview)
+ val modifierKeysContainerView =
+ findViewById<RelativeLayout>(R.id.modifier_keys_container) as ViewGroup
+
+ findViewById<Button>(R.id.settings_button).setOnClickListener {
+ val intent = Intent(this, SettingsActivity::class.java)
+ this.startActivity(intent)
+ }
+ if (terminalGuiSupport()) {
+ displayMenu?.visibility = View.VISIBLE
+ displayMenu?.setEnabled(false)
+
+ displayMenu!!.setOnClickListener {
+ val intent = Intent(this, DisplayActivity::class.java)
+ intent.flags =
+ intent.flags or Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
+ this.startActivity(intent)
+ }
+ }
+
+ modifierKeysController = ModifierKeysController(this, modifierKeysContainerView)
+
+ terminalTabAdapter = TerminalTabAdapter(this)
+ viewPager = findViewById(R.id.pager)
+ viewPager.adapter = terminalTabAdapter
+ viewPager.isUserInputEnabled = false
+ viewPager.offscreenPageLimit = 2
+
+ TabLayoutMediator(tabLayout, viewPager, false, false) { _: TabLayout.Tab?, _: Int -> }
+ .attach()
+
+ addTerminalTab()
+
+ tabAddButton?.setOnClickListener { addTerminalTab() }
+ }
+
+ private fun addTerminalTab() {
+ val tab = tabLayout.newTab()
+ tab.setCustomView(R.layout.tabitem_terminal)
+ viewPager.offscreenPageLimit += 1
+ terminalTabAdapter.addTab()
+ tab.customView!!
+ .findViewById<Button>(R.id.tab_close_button)
+ .setOnClickListener(
+ View.OnClickListener { _: View? ->
+ if (terminalTabAdapter.tabs.size == 1) {
+ finishAndRemoveTask()
+ }
+ viewPager.offscreenPageLimit -= 1
+ terminalTabAdapter.deleteTab(tab.position)
+ tabLayout.removeTab(tab)
+ }
+ )
+ // Add and select the tab
+ tabLayout.addTab(tab, true)
+ }
+
private fun lockOrientationIfNecessary() {
val hasHwQwertyKeyboard = resources.configuration.keyboard == Configuration.KEYBOARD_QWERTY
if (hasHwQwertyKeyboard) {
@@ -175,7 +217,7 @@
private fun getTerminalServiceUrl(ipAddress: String?, port: Int): URL? {
val config = resources.configuration
-
+ // TODO: Always enable screenReaderMode (b/395845063)
val query =
("?fontSize=" +
(config.fontScale * FONT_SIZE_DEFAULT).toInt() +
@@ -196,105 +238,12 @@
}
}
- private fun readClientCertificate() {
- val pke = createOrGetKey()
- writeCertificateToFile(this, pke.certificate)
- privateKey = pke.privateKey
- certificates = arrayOf<X509Certificate>(pke.certificate as X509Certificate)
- }
-
- private fun connectToTerminalService() {
- terminalView.setWebViewClient(
- object : WebViewClient() {
- private var loadFailed = false
- private var requestId: Long = 0
-
- override fun shouldOverrideUrlLoading(
- view: WebView?,
- request: WebResourceRequest?,
- ): Boolean {
- val intent = Intent(Intent.ACTION_VIEW, request?.url)
- intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- startActivity(intent)
- return true
- }
-
- override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
- loadFailed = false
- }
-
- override fun onReceivedError(
- view: WebView,
- request: WebResourceRequest,
- error: WebResourceError,
- ) {
- loadFailed = true
- when (error.getErrorCode()) {
- ERROR_CONNECT,
- ERROR_HOST_LOOKUP,
- ERROR_FAILED_SSL_HANDSHAKE,
- ERROR_TIMEOUT -> {
- view.reload()
- return
- }
-
- else -> {
- val url: String? = request.getUrl().toString()
- val msg = error.getDescription()
- Log.e(TAG, "Failed to load $url: $msg")
- }
- }
- }
-
- override fun onPageFinished(view: WebView, url: String?) {
- if (loadFailed) {
- return
- }
-
- requestId++
- view.postVisualStateCallback(
- requestId,
- object : WebView.VisualStateCallback() {
- override fun onComplete(completedRequestId: Long) {
- if (completedRequestId == requestId) {
- Trace.endAsyncSection("executeTerminal", 0)
- findViewById<View?>(R.id.boot_progress).visibility = View.GONE
- terminalContainer.visibility = View.VISIBLE
- if (terminalGuiSupport()) {
- displayMenu?.setVisible(true)
- displayMenu?.setEnabled(true)
- }
- bootCompleted.open()
- modifierKeysController.update()
- terminalView.mapTouchToMouseEvent()
- }
- }
- },
- )
- }
-
- override fun onReceivedClientCertRequest(
- view: WebView?,
- request: ClientCertRequest,
- ) {
- if (privateKey != null && certificates != null) {
- request.proceed(privateKey, certificates)
- return
- }
- super.onReceivedClientCertRequest(view, request)
- }
-
- override fun onReceivedSslError(
- view: WebView?,
- handler: SslErrorHandler,
- error: SslError?,
- ) {
- // ttyd uses self-signed certificate
- handler.proceed()
- }
- }
- )
-
+ fun connectToTerminalService(terminalView: TerminalView) {
+ if (ipAddress != null && port != null) {
+ val url = getTerminalServiceUrl(ipAddress, port!!)
+ terminalView.loadUrl(url.toString())
+ return
+ }
// TODO: refactor this block as a method
val nsdManager = getSystemService<NsdManager>(NsdManager::class.java)
val info = NsdServiceInfo()
@@ -314,10 +263,10 @@
override fun onServiceUpdated(info: NsdServiceInfo) {
Log.i(TAG, "Service found: $info")
- val ipAddress = info.hostAddresses[0].hostAddress
- val port = info.port
- val url = getTerminalServiceUrl(ipAddress, port)
if (!loaded) {
+ ipAddress = info.hostAddresses[0].hostAddress
+ port = info.port
+ val url = getTerminalServiceUrl(ipAddress, port!!)
loaded = true
nsdManager.unregisterServiceInfoCallback(this)
runOnUiThread(Runnable { terminalView.loadUrl(url.toString()) })
@@ -350,34 +299,10 @@
start(this, Exception("onVmError"))
}
- override fun onCreateOptionsMenu(menu: Menu?): Boolean {
- menuInflater.inflate(R.menu.main_menu, menu)
- displayMenu =
- menu?.findItem(R.id.menu_item_display).also {
- it?.setVisible(terminalGuiSupport())
- it?.setEnabled(false)
- }
- return true
- }
-
- override fun onOptionsItemSelected(item: MenuItem): Boolean {
- val id = item.getItemId()
- if (id == R.id.menu_item_settings) {
- val intent = Intent(this, SettingsActivity::class.java)
- this.startActivity(intent)
- return true
- } else if (id == R.id.menu_item_display) {
- val intent = Intent(this, DisplayActivity::class.java)
- intent.flags =
- intent.flags or Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
- this.startActivity(intent)
- return true
- }
- return super.onOptionsItemSelected(item)
- }
-
override fun onAccessibilityStateChanged(enabled: Boolean) {
- connectToTerminalService()
+ terminalViewModel.terminalViews.forEach { terminalView ->
+ connectToTerminalService(terminalView)
+ }
}
private val installerLauncher =
@@ -462,7 +387,6 @@
Trace.beginAsyncSection("executeTerminal", 0)
run(this, this, notification, getDisplayInfo())
- connectToTerminalService()
}
@VisibleForTesting
@@ -499,21 +423,6 @@
20000 // 20 sec
}
}
-
- private val BTN_KEY_CODE_MAP =
- mapOf(
- R.id.btn_tab to KeyEvent.KEYCODE_TAB, // Alt key sends ESC keycode
- R.id.btn_alt to KeyEvent.KEYCODE_ESCAPE,
- R.id.btn_esc to KeyEvent.KEYCODE_ESCAPE,
- R.id.btn_left to KeyEvent.KEYCODE_DPAD_LEFT,
- R.id.btn_right to KeyEvent.KEYCODE_DPAD_RIGHT,
- R.id.btn_up to KeyEvent.KEYCODE_DPAD_UP,
- R.id.btn_down to KeyEvent.KEYCODE_DPAD_DOWN,
- R.id.btn_home to KeyEvent.KEYCODE_MOVE_HOME,
- R.id.btn_end to KeyEvent.KEYCODE_MOVE_END,
- R.id.btn_pgup to KeyEvent.KEYCODE_PAGE_UP,
- R.id.btn_pgdn to KeyEvent.KEYCODE_PAGE_DOWN,
- )
}
fun getDisplayInfo(): DisplayInfo {
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/ModifierKeysController.kt b/android/TerminalApp/java/com/android/virtualization/terminal/ModifierKeysController.kt
index f8f30f9..ed340d2 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/ModifierKeysController.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/ModifierKeysController.kt
@@ -15,7 +15,6 @@
*/
package com.android.virtualization.terminal
-import android.app.Activity
import android.content.res.Configuration
import android.view.KeyEvent
import android.view.LayoutInflater
@@ -23,20 +22,16 @@
import android.view.ViewGroup
import android.view.WindowInsets
-class ModifierKeysController(
- val activity: Activity,
- val terminalView: TerminalView,
- val parent: ViewGroup,
-) {
+class ModifierKeysController(val activity: MainActivity, val parent: ViewGroup) {
private val window = activity.window
private val keysSingleLine: View
private val keysDoubleLine: View
-
+ private var activeTerminalView: TerminalView? = null
private var keysInSingleLine: Boolean = false
init {
- // Prepare the two modifier keys layout, but don't add them yet because we don't know which
- // layout will be needed.
+ // Prepare the two modifier keys layout, but only attach the double line one since the
+ // keysInSingleLine is set to true by default
val layout = LayoutInflater.from(activity)
keysSingleLine = layout.inflate(R.layout.modifier_keys_singleline, parent, false)
keysDoubleLine = layout.inflate(R.layout.modifier_keys_doubleline, parent, false)
@@ -46,14 +41,25 @@
keysSingleLine.visibility = View.GONE
keysDoubleLine.visibility = View.GONE
+ parent.addView(keysDoubleLine)
// Setup for the update to be called when needed
window.decorView.rootView.setOnApplyWindowInsetsListener { _: View?, insets: WindowInsets ->
update()
insets
}
+ }
- terminalView.setOnFocusChangeListener { _: View, _: Boolean -> update() }
+ fun addTerminalView(terminalView: TerminalView) {
+ terminalView.setOnFocusChangeListener { _: View, onFocus: Boolean ->
+ if (onFocus) {
+ activeTerminalView = terminalView
+ } else {
+ activeTerminalView = null
+ terminalView.disableCtrlKey()
+ }
+ update()
+ }
}
private fun addClickListeners(keys: View) {
@@ -61,15 +67,15 @@
keys
.findViewById<View>(R.id.btn_ctrl)
.setOnClickListener({
- terminalView.mapCtrlKey()
- terminalView.enableCtrlKey()
+ activeTerminalView!!.mapCtrlKey()
+ activeTerminalView!!.enableCtrlKey()
})
val listener =
View.OnClickListener { v: View ->
BTN_KEY_CODE_MAP[v.id]?.also { keyCode ->
- terminalView.dispatchKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, keyCode))
- terminalView.dispatchKeyEvent(KeyEvent(KeyEvent.ACTION_UP, keyCode))
+ activeTerminalView!!.dispatchKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, keyCode))
+ activeTerminalView!!.dispatchKeyEvent(KeyEvent(KeyEvent.ACTION_UP, keyCode))
}
}
@@ -79,38 +85,40 @@
}
fun update() {
- // select single line or double line
- val needSingleLine = needsKeysInSingleLine()
- if (keysInSingleLine != needSingleLine) {
- if (needSingleLine) {
- parent.removeView(keysDoubleLine)
- parent.addView(keysSingleLine)
- } else {
- parent.removeView(keysSingleLine)
- parent.addView(keysDoubleLine)
+ // Pass if no TerminalView focused.
+ if (activeTerminalView == null) {
+ val keys = if (keysInSingleLine) keysSingleLine else keysDoubleLine
+ keys.visibility = View.GONE
+ } else {
+ // select single line or double line
+ val needSingleLine = needsKeysInSingleLine()
+ if (keysInSingleLine != needSingleLine) {
+ if (needSingleLine) {
+ parent.removeView(keysDoubleLine)
+ parent.addView(keysSingleLine)
+ } else {
+ parent.removeView(keysSingleLine)
+ parent.addView(keysDoubleLine)
+ }
+ keysInSingleLine = needSingleLine
}
- keysInSingleLine = needSingleLine
+ // set visibility
+ val needShow = needToShowKeys()
+ val keys = if (keysInSingleLine) keysSingleLine else keysDoubleLine
+ keys.visibility = if (needShow) View.VISIBLE else View.GONE
}
-
- // set visibility
- val needShow = needToShowKeys()
- val keys = if (keysInSingleLine) keysSingleLine else keysDoubleLine
- keys.visibility = if (needShow) View.VISIBLE else View.GONE
}
// Modifier keys are required only when IME is shown and the HW qwerty keyboard is not present
- private fun needToShowKeys(): Boolean {
- val imeShown = activity.window.decorView.rootWindowInsets.isVisible(WindowInsets.Type.ime())
- val hasFocus = terminalView.hasFocus()
- val hasHwQwertyKeyboard =
- activity.resources.configuration.keyboard == Configuration.KEYBOARD_QWERTY
- return imeShown && hasFocus && !hasHwQwertyKeyboard
- }
+ private fun needToShowKeys(): Boolean =
+ activity.window.decorView.rootWindowInsets.isVisible(WindowInsets.Type.ime()) &&
+ activeTerminalView!!.hasFocus() &&
+ !(activity.resources.configuration.keyboard == Configuration.KEYBOARD_QWERTY)
// If terminal's height is less than 30% of the screen height, we need to show modifier keys in
// a single line to save the vertical space
private fun needsKeysInSingleLine(): Boolean =
- (terminalView.height / activity.window.decorView.height.toFloat()) < 0.3f
+ activeTerminalView!!.height.div(activity.window.decorView.height.toFloat()) < 0.3f
companion object {
private val BTN_KEY_CODE_MAP =
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/TerminalTabAdapter.kt b/android/TerminalApp/java/com/android/virtualization/terminal/TerminalTabAdapter.kt
new file mode 100644
index 0000000..9c3cd12
--- /dev/null
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/TerminalTabAdapter.kt
@@ -0,0 +1,60 @@
+/*
+ * 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 androidx.core.os.bundleOf
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentActivity
+import androidx.viewpager2.adapter.FragmentStateAdapter
+import java.util.UUID
+
+class TabMetadata(val id: String)
+
+class TerminalTabAdapter(fragmentActivity: FragmentActivity) :
+ FragmentStateAdapter(fragmentActivity) {
+ val tabs = ArrayList<TabMetadata>()
+
+ override fun createFragment(position: Int): Fragment {
+ val terminalTabFragment = TerminalTabFragment()
+
+ terminalTabFragment.arguments = bundleOf("id" to tabs[position].id)
+ return terminalTabFragment
+ }
+
+ override fun getItemCount(): Int {
+ return tabs.size
+ }
+
+ override fun getItemId(position: Int): Long {
+ return tabs[position].id.hashCode().toLong()
+ }
+
+ override fun containsItem(itemId: Long): Boolean {
+ return tabs.any { it.id.hashCode().toLong() == itemId }
+ }
+
+ fun addTab(): String {
+ val id = UUID.randomUUID().toString()
+ tabs.add(TabMetadata(id))
+ return id
+ }
+
+ fun deleteTab(position: Int) {
+ if (position in 0 until tabs.size) {
+ tabs.removeAt(position)
+ }
+ }
+}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/TerminalTabFragment.kt b/android/TerminalApp/java/com/android/virtualization/terminal/TerminalTabFragment.kt
new file mode 100644
index 0000000..5c01ead
--- /dev/null
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/TerminalTabFragment.kt
@@ -0,0 +1,202 @@
+/*
+ * 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.Intent
+import android.graphics.Bitmap
+import android.net.http.SslError
+import android.os.Bundle
+import android.os.Trace
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.webkit.ClientCertRequest
+import android.webkit.SslErrorHandler
+import android.webkit.WebChromeClient
+import android.webkit.WebResourceError
+import android.webkit.WebResourceRequest
+import android.webkit.WebSettings
+import android.webkit.WebView
+import android.webkit.WebViewClient
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.ViewModelProvider
+import com.android.system.virtualmachine.flags.Flags.terminalGuiSupport
+import com.android.virtualization.terminal.CertificateUtils.createOrGetKey
+import com.android.virtualization.terminal.CertificateUtils.writeCertificateToFile
+import java.security.PrivateKey
+import java.security.cert.X509Certificate
+
+class TerminalTabFragment() : Fragment() {
+ private lateinit var terminalView: TerminalView
+ private lateinit var bootProgressView: View
+ private lateinit var id: String
+ private var certificates: Array<X509Certificate>? = null
+ private var privateKey: PrivateKey? = null
+ private lateinit var terminalViewModel: TerminalViewModel
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?,
+ ): View {
+ val view = inflater.inflate(R.layout.fragment_terminal_tab, container, false)
+ arguments?.let { id = it.getString("id")!! }
+ return view
+ }
+
+ 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()
+ readClientCertificate()
+
+ terminalView.webViewClient = TerminalWebViewClient()
+
+ if (savedInstanceState != null) {
+ terminalView.restoreState(savedInstanceState)
+ } else {
+ (activity as MainActivity).connectToTerminalService(terminalView)
+ }
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(outState)
+ terminalView.saveState(outState)
+ }
+
+ 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.webViewClient = TerminalWebViewClient()
+
+ (activity as MainActivity).modifierKeysController.addTerminalView(terminalView)
+ terminalViewModel.terminalViews.add(terminalView)
+ }
+
+ private inner class TerminalWebViewClient : WebViewClient() {
+ private var loadFailed = false
+ private var requestId: Long = 0
+
+ override fun shouldOverrideUrlLoading(
+ view: WebView?,
+ request: WebResourceRequest?,
+ ): Boolean {
+ val intent = Intent(Intent.ACTION_VIEW, request?.url)
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ startActivity(intent)
+ return true
+ }
+
+ override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
+ loadFailed = false
+ }
+
+ override fun onReceivedError(
+ view: WebView,
+ request: WebResourceRequest,
+ error: WebResourceError,
+ ) {
+ loadFailed = true
+ when (error.getErrorCode()) {
+ ERROR_CONNECT,
+ ERROR_HOST_LOOKUP,
+ ERROR_FAILED_SSL_HANDSHAKE,
+ ERROR_TIMEOUT -> {
+ view.reload()
+ return
+ }
+
+ else -> {
+ val url: String? = request.getUrl().toString()
+ val msg = error.getDescription()
+ Log.e(MainActivity.TAG, "Failed to load $url: $msg")
+ }
+ }
+ }
+
+ override fun onPageFinished(view: WebView, url: String?) {
+ if (loadFailed) {
+ return
+ }
+
+ requestId++
+ view.postVisualStateCallback(
+ requestId,
+ object : WebView.VisualStateCallback() {
+ override fun onComplete(completedRequestId: Long) {
+ if (completedRequestId == requestId) {
+ Trace.endAsyncSection("executeTerminal", 0)
+ bootProgressView.visibility = View.GONE
+ terminalView.visibility = View.VISIBLE
+ terminalView.mapTouchToMouseEvent()
+ updateMainActivity()
+ }
+ }
+ },
+ )
+ }
+
+ override fun onReceivedClientCertRequest(view: WebView?, request: ClientCertRequest) {
+ if (privateKey != null && certificates != null) {
+ request.proceed(privateKey, certificates)
+ return
+ }
+ super.onReceivedClientCertRequest(view, request)
+ }
+
+ override fun onReceivedSslError(
+ view: WebView?,
+ handler: SslErrorHandler,
+ error: SslError?,
+ ) {
+ // ttyd uses self-signed certificate
+ handler.proceed()
+ }
+ }
+
+ private fun updateMainActivity() {
+ val mainActivity = (activity as MainActivity)
+ if (terminalGuiSupport()) {
+ mainActivity.displayMenu!!.visibility = View.VISIBLE
+ mainActivity.displayMenu!!.isEnabled = true
+ }
+ mainActivity.tabAddButton!!.isEnabled = true
+ mainActivity.bootCompleted.open()
+ }
+
+ private fun readClientCertificate() {
+ val pke = createOrGetKey()
+ writeCertificateToFile(activity!!, pke.certificate)
+ privateKey = pke.privateKey
+ certificates = arrayOf<X509Certificate>(pke.certificate as X509Certificate)
+ }
+
+ companion object {
+ const val TAG: String = "VmTerminalApp"
+ }
+
+ override fun onDestroy() {
+ terminalViewModel.terminalViews.remove(terminalView)
+ super.onDestroy()
+ }
+}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/TerminalView.kt b/android/TerminalApp/java/com/android/virtualization/terminal/TerminalView.kt
index 4d9a89d..4b11c1d 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/TerminalView.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/TerminalView.kt
@@ -40,6 +40,7 @@
AccessibilityManager.TouchExplorationStateChangeListener {
private val ctrlKeyHandler: String = readAssetAsString(context, "js/ctrl_key_handler.js")
private val enableCtrlKey: String = readAssetAsString(context, "js/enable_ctrl_key.js")
+ private val disableCtrlKey: String = readAssetAsString(context, "js/disable_ctrl_key.js")
private val touchToMouseHandler: String =
readAssetAsString(context, "js/touch_to_mouse_handler.js")
private val a11yManager =
@@ -65,6 +66,10 @@
this.evaluateJavascript(enableCtrlKey, null)
}
+ fun disableCtrlKey() {
+ this.evaluateJavascript(disableCtrlKey, null)
+ }
+
override fun onAccessibilityStateChanged(enabled: Boolean) {
Log.d(TAG, "accessibility $enabled")
adjustToA11yStateChange()
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/TerminalViewModel.kt b/android/TerminalApp/java/com/android/virtualization/terminal/TerminalViewModel.kt
new file mode 100644
index 0000000..4a69f75
--- /dev/null
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/TerminalViewModel.kt
@@ -0,0 +1,22 @@
+/*
+ * 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 androidx.lifecycle.ViewModel
+
+class TerminalViewModel : ViewModel() {
+ val terminalViews: MutableSet<TerminalView> = mutableSetOf()
+}
diff --git a/android/TerminalApp/res/drawable/background_tabitem_selected.xml b/android/TerminalApp/res/drawable/background_tabitem_selected.xml
new file mode 100644
index 0000000..8784304
--- /dev/null
+++ b/android/TerminalApp/res/drawable/background_tabitem_selected.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+ -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <corners
+ android:topLeftRadius="10dp"
+ android:topRightRadius="10dp" />
+</shape>
diff --git a/android/TerminalApp/res/layout/activity_headless.xml b/android/TerminalApp/res/layout/activity_headless.xml
index e18aa5c..bf84833 100644
--- a/android/TerminalApp/res/layout/activity_headless.xml
+++ b/android/TerminalApp/res/layout/activity_headless.xml
@@ -14,51 +14,83 @@
limitations under the License.
-->
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/terminal_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:orientation="vertical"
android:fitsSystemWindows="true"
tools:context=".MainActivity">
- <com.google.android.material.appbar.MaterialToolbar
- android:id="@+id/toolbar"
+
+ <HorizontalScrollView
+ android:id="@+id/tab_scrollview"
android:layout_width="match_parent"
- android:layout_height="?attr/actionBarSize"
- app:layout_constraintTop_toTopOf="parent"/>
- <FrameLayout
- android:layout_width="match_parent"
- android:layout_height="match_parent">
+ android:layout_height="wrap_content"
+ android:layout_alignParentStart="true"
+ android:layout_alignParentTop="true"
+ android:layout_toStartOf="@id/settings_button"
+ android:scrollbars="none">
+
<LinearLayout
- android:id="@+id/boot_progress"
- android:orientation="vertical"
- android:gravity="center"
- android:layout_gravity="center"
android:layout_width="wrap_content"
- android:layout_height="wrap_content">
- <com.google.android.material.textview.MaterialTextView
- android:text="@string/vm_creation_message"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <com.google.android.material.tabs.TabLayout
+ android:id="@+id/tab_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_marginBottom="5dp"/>
- <com.google.android.material.progressindicator.CircularProgressIndicator
- android:indeterminate="true"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"/>
- </LinearLayout>
- <LinearLayout
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:layout_marginBottom="5dp"
- android:orientation="vertical"
- android:visibility="gone" >
- <com.android.virtualization.terminal.TerminalView
- android:id="@+id/webview"
- android:layout_width="match_parent"
- android:layout_height="0dp"
- android:layout_weight="1" />
- </LinearLayout>
- </FrameLayout>
+ android:elevation="0dp"
+ app:tabIndicator="@drawable/background_tabitem_selected"
+ app:tabIndicatorColor="@color/material_on_surface_stroke"
+ app:tabRippleColor="@null"
+ app:tabIndicatorHeight="48dp"
+ app:tabPaddingStart="0dp"
+ app:tabPaddingEnd="0dp"/>
-</LinearLayout>
+ <Button
+ android:id="@+id/tab_add_button"
+ style="?attr/materialIconButtonStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:enabled="false"
+ app:icon="@drawable/ic_add" />
+ </LinearLayout>
+ </HorizontalScrollView>
+
+ <Button
+ android:id="@+id/settings_button"
+ style="?attr/materialIconButtonStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentTop="true"
+ android:layout_toStartOf="@id/display_button"
+ app:icon="@drawable/ic_settings" />
+
+ <Button
+ android:id="@+id/display_button"
+ style="?attr/materialIconButtonStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentEnd="true"
+ android:layout_alignParentTop="true"
+ app:icon="@drawable/ic_display" />
+
+
+ <androidx.viewpager2.widget.ViewPager2
+ android:id="@+id/pager"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_alignParentStart="true"
+ android:layout_above="@+id/modifier_keys_container"
+ android:layout_below="@id/settings_button"/>
+
+ <LinearLayout
+ android:id="@+id/modifier_keys_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentStart="true"
+ android:orientation="vertical"/>
+</RelativeLayout>
diff --git a/android/TerminalApp/res/layout/fragment_terminal_tab.xml b/android/TerminalApp/res/layout/fragment_terminal_tab.xml
new file mode 100644
index 0000000..bdf3d83
--- /dev/null
+++ b/android/TerminalApp/res/layout/fragment_terminal_tab.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+ -->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <LinearLayout
+ android:id="@+id/boot_progress"
+ android:orientation="vertical"
+ android:gravity="center"
+ android:layout_gravity="center"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+ <com.google.android.material.textview.MaterialTextView
+ android:text="@string/vm_creation_message"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="5dp"/>
+ <com.google.android.material.progressindicator.CircularProgressIndicator
+ android:indeterminate="true"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+ </LinearLayout>
+ <com.android.virtualization.terminal.TerminalView
+ android:id="@+id/webview"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:visibility="gone"
+ android:layout_marginBottom="5dp"/>
+</FrameLayout>
\ No newline at end of file
diff --git a/android/TerminalApp/res/layout/tabitem_terminal.xml b/android/TerminalApp/res/layout/tabitem_terminal.xml
new file mode 100644
index 0000000..92e3802
--- /dev/null
+++ b/android/TerminalApp/res/layout/tabitem_terminal.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+ -->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="200dp"
+ android:layout_height="48dp">
+
+ <TextView
+ android:id="@+id/tab_title"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_alignParentStart="true"
+ android:layout_alignParentTop="true"
+ android:layout_toStartOf="@id/tab_close_button"
+ android:gravity="center"
+ android:padding="8dp"
+ android:text="@string/tab_default_title"/>
+
+ <Button
+ style="?attr/materialIconButtonStyle"
+ android:id="@+id/tab_close_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentTop="true"
+ android:layout_alignParentEnd="true"
+ app:icon="@drawable/ic_close"
+ android:clickable="true"
+ android:focusable="true"/>
+
+</RelativeLayout>
\ No newline at end of file
diff --git a/android/TerminalApp/res/values/strings.xml b/android/TerminalApp/res/values/strings.xml
index 44009c3..fc8d036 100644
--- a/android/TerminalApp/res/values/strings.xml
+++ b/android/TerminalApp/res/values/strings.xml
@@ -181,4 +181,7 @@
<!-- This is the name of the notification channel for system events [CHAR LIMIT=none] -->
<string name="notification_channel_system_events_name">System events</string>
+
+ <!-- Default title of a terminal tab [CHAR LIMIT=10] -->
+ <string name="tab_default_title">Tab</string>
</resources>
diff --git a/android/composd/aidl/android/system/composd/IIsolatedCompilationService.aidl b/android/composd/aidl/android/system/composd/IIsolatedCompilationService.aidl
index 3748899..d7757db 100644
--- a/android/composd/aidl/android/system/composd/IIsolatedCompilationService.aidl
+++ b/android/composd/aidl/android/system/composd/IIsolatedCompilationService.aidl
@@ -35,7 +35,7 @@
* callback, unless the returned ICompilationTask is cancelled. The caller should maintain
* a reference to the ICompilationTask until compilation completes or is cancelled.
*/
- ICompilationTask startStagedApexCompile(ICompilationTaskCallback callback);
+ ICompilationTask startStagedApexCompile(ICompilationTaskCallback callback, String os);
/**
* Run odrefresh in a test instance of CompOS until completed or failed.
diff --git a/android/composd/src/instance_manager.rs b/android/composd/src/instance_manager.rs
index d1b0b99..a7154ec 100644
--- a/android/composd/src/instance_manager.rs
+++ b/android/composd/src/instance_manager.rs
@@ -39,10 +39,11 @@
Self { service, state: Default::default() }
}
- pub fn start_current_instance(&self) -> Result<CompOsInstance> {
+ pub fn start_current_instance(&self, os: &str) -> Result<CompOsInstance> {
let mut vm_parameters = new_vm_parameters()?;
vm_parameters.name = String::from("Composd");
vm_parameters.prefer_staged = true;
+ vm_parameters.os = os.to_owned();
self.start_instance(CURRENT_INSTANCE_DIR, vm_parameters)
}
diff --git a/android/composd/src/service.rs b/android/composd/src/service.rs
index 3cc40af..1e38eee 100644
--- a/android/composd/src/service.rs
+++ b/android/composd/src/service.rs
@@ -51,9 +51,10 @@
fn startStagedApexCompile(
&self,
callback: &Strong<dyn ICompilationTaskCallback>,
+ os: &str,
) -> binder::Result<Strong<dyn ICompilationTask>> {
check_permissions()?;
- to_binder_result(self.do_start_staged_apex_compile(callback))
+ to_binder_result(self.do_start_staged_apex_compile(callback, os))
}
fn startTestCompile(
@@ -76,8 +77,10 @@
fn do_start_staged_apex_compile(
&self,
callback: &Strong<dyn ICompilationTaskCallback>,
+ os: &str,
) -> Result<Strong<dyn ICompilationTask>> {
- let comp_os = self.instance_manager.start_current_instance().context("Starting CompOS")?;
+ let comp_os =
+ self.instance_manager.start_current_instance(os).context("Starting CompOS")?;
let target_dir_name = PENDING_ARTIFACTS_SUBDIR.to_owned();
let task = OdrefreshTask::start(
diff --git a/android/composd_cmd/composd_cmd.rs b/android/composd_cmd/composd_cmd.rs
index 6281bd0..c944c17 100644
--- a/android/composd_cmd/composd_cmd.rs
+++ b/android/composd_cmd/composd_cmd.rs
@@ -39,7 +39,11 @@
#[derive(Parser)]
enum Actions {
/// Compile classpath for real. Output can be used after a reboot.
- StagedApexCompile {},
+ StagedApexCompile {
+ /// OS for the VM.
+ #[clap(long, default_value = "microdroid")]
+ os: String,
+ },
/// Compile classpath in a debugging VM. Output is ignored.
TestCompile {
@@ -59,7 +63,7 @@
ProcessState::start_thread_pool();
match action {
- Actions::StagedApexCompile {} => run_staged_apex_compile()?,
+ Actions::StagedApexCompile { os } => run_staged_apex_compile(&os)?,
Actions::TestCompile { prefer_staged, os } => run_test_compile(prefer_staged, &os)?,
}
@@ -116,8 +120,8 @@
}
}
-fn run_staged_apex_compile() -> Result<()> {
- run_async_compilation(|service, callback| service.startStagedApexCompile(callback))
+fn run_staged_apex_compile(os: &str) -> Result<()> {
+ run_async_compilation(|service, callback| service.startStagedApexCompile(callback, os))
}
fn run_test_compile(prefer_staged: bool, os: &str) -> Result<()> {
diff --git a/android/virtmgr/src/aidl.rs b/android/virtmgr/src/aidl.rs
index cc6141e..1a263bd 100644
--- a/android/virtmgr/src/aidl.rs
+++ b/android/virtmgr/src/aidl.rs
@@ -523,17 +523,7 @@
.or_service_specific_exception(-1)
}
};
- let expected_exe_path = Path::new(&early_vm.path);
- if expected_exe_path != calling_exe_path
- && Path::new("/system").join(expected_exe_path) != calling_exe_path
- {
- return Err(anyhow!(
- "VM '{name}' in partition '{calling_partition}' must be created with '{}', not '{}'",
- &early_vm.path,
- calling_exe_path.display()
- ))
- .or_service_specific_exception(-1);
- }
+ early_vm.check_exe_paths_match(calling_exe_path)?;
let cid = early_vm.cid as Cid;
let temp_dir = PathBuf::from(format!("/mnt/vm/early/{cid}"));
@@ -2321,6 +2311,29 @@
early_vm: Vec<EarlyVm>,
}
+impl EarlyVm {
+ /// Verifies that the provided executable path matches the expected path stored in the XML
+ /// configuration.
+ /// If the provided path starts with `/system`, it will be stripped before comparison.
+ fn check_exe_paths_match<P: AsRef<Path>>(&self, calling_exe_path: P) -> binder::Result<()> {
+ let actual_path = calling_exe_path.as_ref();
+ if Path::new(&self.path)
+ == Path::new("/").join(actual_path.strip_prefix("/system").unwrap_or(actual_path))
+ {
+ return Ok(());
+ }
+ Err(Status::new_service_specific_error_str(
+ -1,
+ Some(format!(
+ "Early VM '{}' executable paths do not match. Expected: {}. Found: {:?}.",
+ self.name,
+ self.path,
+ actual_path.display()
+ )),
+ ))
+ }
+}
+
static EARLY_VMS_CACHE: LazyLock<Mutex<HashMap<CallingPartition, Vec<EarlyVm>>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));
@@ -2749,6 +2762,39 @@
}
#[test]
+ fn early_vm_exe_paths_match_succeeds_with_same_paths() {
+ let early_vm = EarlyVm {
+ name: "vm_demo_native_early".to_owned(),
+ cid: 123,
+ path: "/system_ext/bin/vm_demo_native_early".to_owned(),
+ };
+ let calling_exe_path = "/system_ext/bin/vm_demo_native_early";
+ assert!(early_vm.check_exe_paths_match(calling_exe_path).is_ok())
+ }
+
+ #[test]
+ fn early_vm_exe_paths_match_succeeds_with_calling_exe_path_from_system() {
+ let early_vm = EarlyVm {
+ name: "vm_demo_native_early".to_owned(),
+ cid: 123,
+ path: "/system_ext/bin/vm_demo_native_early".to_owned(),
+ };
+ let calling_exe_path = "/system/system_ext/bin/vm_demo_native_early";
+ assert!(early_vm.check_exe_paths_match(calling_exe_path).is_ok())
+ }
+
+ #[test]
+ fn early_vm_exe_paths_match_fails_with_unmatched_paths() {
+ let early_vm = EarlyVm {
+ name: "vm_demo_native_early".to_owned(),
+ cid: 123,
+ path: "/system_ext/bin/vm_demo_native_early".to_owned(),
+ };
+ let calling_exe_path = "/system/etc/system_ext/bin/vm_demo_native_early";
+ assert!(early_vm.check_exe_paths_match(calling_exe_path).is_err())
+ }
+
+ #[test]
fn test_duplicated_early_vms() -> Result<()> {
let tmp_dir = tempfile::TempDir::new()?;
let tmp_dir_path = tmp_dir.path().to_owned();
diff --git a/android/virtmgr/src/crosvm.rs b/android/virtmgr/src/crosvm.rs
index 8500421..77710c3 100644
--- a/android/virtmgr/src/crosvm.rs
+++ b/android/virtmgr/src/crosvm.rs
@@ -21,7 +21,7 @@
use binder::ParcelFileDescriptor;
use command_fds::CommandFdExt;
use libc::{sysconf, _SC_CLK_TCK};
-use log::{debug, error, info};
+use log::{debug, error, info, warn};
use semver::{Version, VersionReq};
use nix::{fcntl::OFlag, unistd::pipe2, unistd::Uid, unistd::User};
use regex::{Captures, Regex};
@@ -628,6 +628,7 @@
fn monitor_vm_status(&self, child: Arc<SharedChild>) {
let pid = child.id();
+ let mut metric_countdown = 0;
loop {
{
@@ -637,23 +638,34 @@
break;
}
- let mut vm_metric = self.vm_metric.lock().unwrap();
+ if metric_countdown > 0 {
+ metric_countdown -= 1;
+ } else {
+ metric_countdown = 10;
+ let mut vm_metric = self.vm_metric.lock().unwrap();
- // Get CPU Information
- match get_guest_time(pid) {
- Ok(guest_time) => vm_metric.cpu_guest_time = Some(guest_time),
- Err(e) => error!("Failed to get guest CPU time: {e:?}"),
- }
-
- // Get Memory Information
- match get_rss(pid) {
- Ok(rss) => {
- vm_metric.rss = match &vm_metric.rss {
- Some(x) => Some(Rss::extract_max(x, &rss)),
- None => Some(rss),
+ // Get CPU Information
+ match get_guest_time(pid) {
+ Ok(guest_time) => vm_metric.cpu_guest_time = Some(guest_time),
+ Err(e) => {
+ metric_countdown = 0;
+ warn!("Failed to get guest CPU time: {}", e);
}
}
- Err(e) => error!("Failed to get guest RSS: {}", e),
+
+ // Get Memory Information
+ match get_rss(pid) {
+ Ok(rss) => {
+ vm_metric.rss = match &vm_metric.rss {
+ Some(x) => Some(Rss::extract_max(x, &rss)),
+ None => Some(rss),
+ }
+ }
+ Err(e) => {
+ metric_countdown = 0;
+ warn!("Failed to get guest RSS: {}", e);
+ }
+ }
}
}
@@ -845,6 +857,9 @@
}
let guest_time_ticks = data_list[42].parse::<i64>()?;
+ if guest_time_ticks == 0 {
+ bail!("zero value is measured on elapsed CPU guest_time");
+ }
// SAFETY: It just returns an integer about CPU tick information.
let ticks_per_sec = unsafe { sysconf(_SC_CLK_TCK) };
Ok(guest_time_ticks * MILLIS_PER_SEC / ticks_per_sec)
@@ -875,7 +890,12 @@
rss_crosvm_total += rss;
}
}
-
+ if rss_crosvm_total == 0 {
+ bail!("zero value is measured on RSS of crosvm");
+ }
+ if rss_vm_total == 0 {
+ bail!("zero value is measured on RSS of VM");
+ }
Ok(Rss { vm: rss_vm_total, crosvm: rss_crosvm_total })
}
diff --git a/build/debian/build.sh b/build/debian/build.sh
index 6facfcf..616dd00 100755
--- a/build/debian/build.sh
+++ b/build/debian/build.sh
@@ -269,7 +269,7 @@
--extract "${dsc_file}"
pushd "linux-${debian_kver%-*}" > /dev/null
- local kpatches_src="$SCRIPT_DIR/kernel_patches"
+ local kpatches_src="$SCRIPT_DIR/kernel/patches"
cp -r "${kpatches_src}/avf" debian/patches/
cat "${kpatches_src}/series" >> debian/patches/series
./debian/rules orig
@@ -282,9 +282,8 @@
# 2. Define our custom flavour and regenerate control file
# NOTE: Our flavour extends Debian's `cloud` config on the `none` featureset.
- cat > debian/config/${debian_arch}/config.${debarch_flavour} <<EOF
-# TODO: Add our custom kernel config to this file
-EOF
+ cp "$SCRIPT_DIR/kernel/config" \
+ debian/config/${debian_arch}/config.${debarch_flavour}
sed -z "s;\[base\]\nflavours:;[base]\nflavours:\n ${debarch_flavour};" \
-i debian/config/${debian_arch}/none/defines
diff --git a/build/debian/fai_config/files/usr/local/bin/enable_display/AVF b/build/debian/fai_config/files/usr/local/bin/enable_display/AVF
index 69dce6a..76f9f97 100644
--- a/build/debian/fai_config/files/usr/local/bin/enable_display/AVF
+++ b/build/debian/fai_config/files/usr/local/bin/enable_display/AVF
@@ -1,4 +1,6 @@
#!/bin/bash
sudo systemd-run --collect -E XDG_SESSION_TYPE=wayland --uid=1000 -p PAMName=login -p TTYPath=/dev/tty7 sleep 1d
systemctl --user start weston
-export DISPLAY=:0
\ No newline at end of file
+export DISPLAY=:0
+export MESA_LOADER_DRIVER_OVERRIDE=zink
+export LIBGL_ALWAYS_SOFTWARE=1
\ No newline at end of file
diff --git a/build/debian/fai_config/scripts/AVF/20-useradd b/build/debian/fai_config/scripts/AVF/20-useradd
index 1c93772..b92648a 100755
--- a/build/debian/fai_config/scripts/AVF/20-useradd
+++ b/build/debian/fai_config/scripts/AVF/20-useradd
@@ -1,4 +1,4 @@
#!/bin/bash
-$ROOTCMD useradd -m -u 1000 -N -G sudo -s /usr/bin/bash droid
+$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
diff --git a/build/debian/kernel/config b/build/debian/kernel/config
new file mode 100644
index 0000000..1ba603c
--- /dev/null
+++ b/build/debian/kernel/config
@@ -0,0 +1 @@
+CONFIG_DRM=m
diff --git a/build/debian/kernel_patches/avf/arm64-balloon.patch b/build/debian/kernel/patches/avf/arm64-balloon.patch
similarity index 100%
rename from build/debian/kernel_patches/avf/arm64-balloon.patch
rename to build/debian/kernel/patches/avf/arm64-balloon.patch
diff --git a/build/debian/kernel_patches/series b/build/debian/kernel/patches/series
similarity index 100%
rename from build/debian/kernel_patches/series
rename to build/debian/kernel/patches/series
diff --git a/build/debian/vm_config.json.aarch64 b/build/debian/vm_config.json.aarch64
index 96254f8..463583f 100644
--- a/build/debian/vm_config.json.aarch64
+++ b/build/debian/vm_config.json.aarch64
@@ -35,5 +35,8 @@
"console_out": true,
"console_input_device": "ttyS0",
"network": true,
- "auto_memory_balloon": true
+ "auto_memory_balloon": true,
+ "gpu": {
+ "backend": "2d"
+ }
}
diff --git a/build/debian/vm_config.json.x86_64 b/build/debian/vm_config.json.x86_64
index c34a0f2..bc4e00a 100644
--- a/build/debian/vm_config.json.x86_64
+++ b/build/debian/vm_config.json.x86_64
@@ -44,5 +44,8 @@
"console_out": true,
"console_input_device": "ttyS0",
"network": true,
- "auto_memory_balloon": true
+ "auto_memory_balloon": true,
+ "gpu": {
+ "backend": "2d"
+ }
}
diff --git a/guest/pvmfw/README.md b/guest/pvmfw/README.md
index 766a923..652ca90 100644
--- a/guest/pvmfw/README.md
+++ b/guest/pvmfw/README.md
@@ -461,7 +461,12 @@
- `secretkeeper_protection`: pvmfw defers rollback protection to the guest
- `supports_uefi_boot`: pvmfw boots the VM as a EFI payload (experimental)
- `trusty_security_vm`: pvmfw skips rollback protection
-- `"com.android.virt.page_size"`: the guest page size in KiB (optional, defaults to 4)
+- `"com.android.virt.page_size"`: (optional) the guest page size in KiB, defaults to 4
+- `"com.android.virt.name"`: (optional) VM name, used as the
+ [`component_name`][dice-comp-name] (defaults to `"vm_entry"`) in the guest
+ DICE certificate and to identify special VMs
+
+[dice-comp-name]: https://cs.android.com/android/platform/superproject/main/+/main:external/open-dice/docs/android.md;l=81;drc=6d511e9533eac05d64d47fcd78ac5d881e72c3de
## Development
diff --git a/guest/pvmfw/avb/Android.bp b/guest/pvmfw/avb/Android.bp
index 141c1d2..0d55d7c 100644
--- a/guest/pvmfw/avb/Android.bp
+++ b/guest/pvmfw/avb/Android.bp
@@ -37,6 +37,7 @@
":test_image_with_one_hashdesc",
":test_image_with_non_initrd_hashdesc",
":test_image_with_initrd_and_non_initrd_desc",
+ ":test_image_with_name",
":test_image_with_invalid_page_size",
":test_image_with_negative_page_size",
":test_image_with_overflow_page_size",
@@ -123,6 +124,20 @@
}
avb_add_hash_footer {
+ name: "test_image_with_name",
+ src: ":unsigned_test_image",
+ partition_name: "boot",
+ private_key: ":pvmfw_sign_key",
+ salt: "2134",
+ props: [
+ {
+ name: "com.android.virt.name",
+ value: "test_vm_name",
+ },
+ ],
+}
+
+avb_add_hash_footer {
name: "test_image_with_invalid_page_size",
src: ":unsigned_test_image",
partition_name: "boot",
diff --git a/guest/pvmfw/avb/src/error.rs b/guest/pvmfw/avb/src/error.rs
index 1307e15..eb82837 100644
--- a/guest/pvmfw/avb/src/error.rs
+++ b/guest/pvmfw/avb/src/error.rs
@@ -30,6 +30,8 @@
UnknownVbmetaProperty,
/// VBMeta has invalid page_size property.
InvalidPageSize,
+ /// VBMeta has invalid VM name property.
+ InvalidVmName,
}
impl From<SlotVerifyError<'_>> for PvmfwVerifyError {
@@ -54,6 +56,7 @@
}
Self::UnknownVbmetaProperty => write!(f, "Unknown vbmeta property"),
Self::InvalidPageSize => write!(f, "Invalid page_size property"),
+ Self::InvalidVmName => write!(f, "Invalid name property"),
}
}
}
diff --git a/guest/pvmfw/avb/src/verify.rs b/guest/pvmfw/avb/src/verify.rs
index 8810696..6a3d7de 100644
--- a/guest/pvmfw/avb/src/verify.rs
+++ b/guest/pvmfw/avb/src/verify.rs
@@ -17,7 +17,7 @@
use crate::ops::{Ops, Payload};
use crate::partition::PartitionName;
use crate::PvmfwVerifyError;
-use alloc::vec::Vec;
+use alloc::{string::String, vec::Vec};
use avb::{
Descriptor, DescriptorError, DescriptorResult, HashDescriptor, PartitionData, SlotVerifyError,
SlotVerifyNoDataResult, VbmetaData,
@@ -47,6 +47,8 @@
pub rollback_index: u64,
/// Page size of kernel, if present.
pub page_size: Option<usize>,
+ /// Name of the guest payload, if present.
+ pub name: Option<String>,
}
impl VerifiedBootData<'_> {
@@ -238,6 +240,18 @@
Ok(Some(size))
}
+/// Returns the indicated payload name, if present.
+fn read_name(vbmeta_data: &VbmetaData) -> Result<Option<String>, PvmfwVerifyError> {
+ let Some(property) = vbmeta_data.get_property_value("com.android.virt.name") else {
+ return Ok(None);
+ };
+ let name = str::from_utf8(property).map_err(|_| PvmfwVerifyError::InvalidVmName)?;
+ if name.is_empty() {
+ return Err(PvmfwVerifyError::InvalidVmName);
+ }
+ Ok(Some(name.into()))
+}
+
/// Verifies the given initrd partition, and checks that the resulting contents looks like expected.
fn verify_initrd(
ops: &mut Ops,
@@ -275,6 +289,7 @@
let hash_descriptors = HashDescriptors::get(&descriptors)?;
let capabilities = Capability::get_capabilities(vbmeta_image)?;
let page_size = read_page_size(vbmeta_image)?;
+ let name = read_name(vbmeta_image)?;
if initrd.is_none() {
hash_descriptors.verify_no_initrd()?;
@@ -286,6 +301,7 @@
capabilities,
rollback_index,
page_size,
+ name,
});
}
@@ -307,5 +323,6 @@
capabilities,
rollback_index,
page_size,
+ name,
})
}
diff --git a/guest/pvmfw/avb/tests/api_test.rs b/guest/pvmfw/avb/tests/api_test.rs
index 3027c47..b3899d9 100644
--- a/guest/pvmfw/avb/tests/api_test.rs
+++ b/guest/pvmfw/avb/tests/api_test.rs
@@ -27,6 +27,7 @@
use utils::*;
const TEST_IMG_WITH_ONE_HASHDESC_PATH: &str = "test_image_with_one_hashdesc.img";
+const TEST_IMG_WITH_NAME_PATH: &str = "test_image_with_name.img";
const TEST_IMG_WITH_INVALID_PAGE_SIZE_PATH: &str = "test_image_with_invalid_page_size.img";
const TEST_IMG_WITH_NEGATIVE_PAGE_SIZE_PATH: &str = "test_image_with_negative_page_size.img";
const TEST_IMG_WITH_OVERFLOW_PAGE_SIZE_PATH: &str = "test_image_with_overflow_page_size.img";
@@ -102,6 +103,7 @@
capabilities: vec![],
rollback_index: 0,
page_size: None,
+ name: None,
};
assert_eq!(expected_boot_data, verified_boot_data);
@@ -147,6 +149,7 @@
capabilities: vec![Capability::RemoteAttest],
rollback_index: 0,
page_size: None,
+ name: None,
};
assert_eq!(expected_boot_data, verified_boot_data);
@@ -247,6 +250,18 @@
}
#[test]
+fn kernel_has_expected_valid_name() {
+ let kernel = fs::read(TEST_IMG_WITH_NAME_PATH).unwrap();
+ assert_eq!(read_name(&kernel), Ok(Some("test_vm_name".to_owned())));
+}
+
+#[test]
+fn kernel_has_expected_missing_name() {
+ let kernel = fs::read(TEST_IMG_WITH_ONE_HASHDESC_PATH).unwrap();
+ assert_eq!(read_name(&kernel), Ok(None));
+}
+
+#[test]
fn kernel_has_expected_page_size_invalid() {
let kernel = fs::read(TEST_IMG_WITH_INVALID_PAGE_SIZE_PATH).unwrap();
assert_eq!(read_page_size(&kernel), Err(PvmfwVerifyError::InvalidPageSize));
@@ -483,6 +498,7 @@
capabilities: vec![],
rollback_index: 5,
page_size: None,
+ name: None,
};
assert_eq!(expected_boot_data, verified_boot_data);
Ok(())
diff --git a/guest/pvmfw/avb/tests/utils.rs b/guest/pvmfw/avb/tests/utils.rs
index 7282f3e..227daa2 100644
--- a/guest/pvmfw/avb/tests/utils.rs
+++ b/guest/pvmfw/avb/tests/utils.rs
@@ -28,6 +28,7 @@
use std::{
fs,
mem::{size_of, transmute, MaybeUninit},
+ string::String,
};
const MICRODROID_KERNEL_IMG_PATH: &str = "microdroid_kernel";
@@ -134,6 +135,7 @@
capabilities,
rollback_index: 1,
page_size,
+ name: None,
};
assert_eq!(expected_boot_data, verified_boot_data);
@@ -166,12 +168,23 @@
capabilities,
rollback_index: expected_rollback_index,
page_size,
+ name: None,
};
assert_eq!(expected_boot_data, verified_boot_data);
Ok(())
}
+pub fn read_name(kernel: &[u8]) -> Result<Option<String>, PvmfwVerifyError> {
+ let public_key = load_trusted_public_key().unwrap();
+ let verified_boot_data = verify_payload(
+ kernel,
+ None, // initrd
+ &public_key,
+ )?;
+ Ok(verified_boot_data.name)
+}
+
pub fn read_page_size(kernel: &[u8]) -> Result<Option<usize>, PvmfwVerifyError> {
let public_key = load_trusted_public_key().unwrap();
let verified_boot_data = verify_payload(
diff --git a/guest/pvmfw/src/dice.rs b/guest/pvmfw/src/dice.rs
index f49fedb..49a3807 100644
--- a/guest/pvmfw/src/dice.rs
+++ b/guest/pvmfw/src/dice.rs
@@ -16,6 +16,7 @@
extern crate alloc;
use alloc::format;
+use alloc::string::String;
use alloc::vec::Vec;
use ciborium::cbor;
use ciborium::Value;
@@ -83,6 +84,7 @@
pub mode: DiceMode,
pub security_version: u64,
pub rkp_vm_marker: bool,
+ component_name: String,
}
impl PartialInputs {
@@ -90,12 +92,13 @@
let code_hash = to_dice_hash(data)?;
let auth_hash = hash(data.public_key)?;
let mode = to_dice_mode(data.debug_level);
+ let component_name = data.name.clone().unwrap_or(String::from("vm_entry"));
// We use rollback_index from vbmeta as the security_version field in dice certificate.
let security_version = data.rollback_index;
let rkp_vm_marker = data.has_capability(Capability::RemoteAttest)
|| data.has_capability(Capability::TrustySecurityVm);
- Ok(Self { code_hash, auth_hash, mode, security_version, rkp_vm_marker })
+ Ok(Self { code_hash, auth_hash, mode, security_version, rkp_vm_marker, component_name })
}
pub fn write_next_bcc(
@@ -156,7 +159,7 @@
fn generate_config_descriptor(&self, instance_hash: Option<Hash>) -> Result<Vec<u8>> {
let mut config = Vec::with_capacity(4);
- config.push((cbor!(COMPONENT_NAME_KEY)?, cbor!("vm_entry")?));
+ config.push((cbor!(COMPONENT_NAME_KEY)?, cbor!(self.component_name.as_str())?));
config.push((cbor!(SECURITY_VERSION_KEY)?, cbor!(self.security_version)?));
if self.rkp_vm_marker {
config.push((cbor!(RKP_VM_MARKER_KEY)?, Value::Null))
@@ -200,6 +203,7 @@
kernel_digest: [1u8; size_of::<Digest>()],
initrd_digest: Some([2u8; size_of::<Digest>()]),
public_key: b"public key",
+ name: None,
capabilities: vec![],
rollback_index: 42,
page_size: None,
@@ -249,12 +253,13 @@
}
#[test]
- fn rkp_vm_config_descriptor_has_rkp_vm_marker() {
+ fn rkp_vm_config_descriptor_has_rkp_vm_marker_and_component_name() {
let vb_data =
VerifiedBootData { capabilities: vec![Capability::RemoteAttest], ..BASE_VB_DATA };
let inputs = PartialInputs::new(&vb_data).unwrap();
let config_map = decode_config_descriptor(&inputs, Some(HASH));
+ assert_eq!(config_map.get(&COMPONENT_NAME_KEY).unwrap().as_text().unwrap(), "vm_entry");
assert!(config_map.get(&RKP_VM_MARKER_KEY).unwrap().is_null());
}
diff --git a/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachine.java b/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachine.java
index 5f634ef..af313a1 100644
--- a/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachine.java
+++ b/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachine.java
@@ -276,28 +276,16 @@
@Override
public void onTrimMemory(int level) {
- int percent;
+ /* Treat level < TRIM_MEMORY_UI_HIDDEN as generic low-memory warnings */
+ int percent = 10;
- switch (level) {
- case ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL:
- percent = 50;
- break;
- case ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW:
- percent = 30;
- break;
- case ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE:
- percent = 10;
- break;
- case ComponentCallbacks2.TRIM_MEMORY_BACKGROUND:
- case ComponentCallbacks2.TRIM_MEMORY_MODERATE:
- case ComponentCallbacks2.TRIM_MEMORY_COMPLETE:
- /* Release as much memory as we can. The app is on the LMKD LRU kill list. */
- percent = 50;
- break;
- default:
- /* Treat unrecognised messages as generic low-memory warnings. */
- percent = 30;
- break;
+ if (level >= ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) {
+ percent = 30;
+ }
+
+ if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {
+ /* Release as much memory as we can. The app is on the LMKD LRU kill list. */
+ percent = 50;
}
synchronized (mLock) {
diff --git a/libs/service-compos/java/com/android/server/compos/IsolatedCompilationJobService.java b/libs/service-compos/java/com/android/server/compos/IsolatedCompilationJobService.java
index adc0300..3033991 100644
--- a/libs/service-compos/java/com/android/server/compos/IsolatedCompilationJobService.java
+++ b/libs/service-compos/java/com/android/server/compos/IsolatedCompilationJobService.java
@@ -174,7 +174,7 @@
}
try {
- ICompilationTask composTask = composd.startStagedApexCompile(this);
+ ICompilationTask composTask = composd.startStagedApexCompile(this, "microdroid");
mMetrics.onCompilationStarted();
mTask.set(composTask);
composTask.asBinder().linkToDeath(this, 0);
diff --git a/tests/benchmark_hostside/java/android/avf/test/AVFHostTestCase.java b/tests/benchmark_hostside/java/android/avf/test/AVFHostTestCase.java
index e2956f2..003c3f0 100644
--- a/tests/benchmark_hostside/java/android/avf/test/AVFHostTestCase.java
+++ b/tests/benchmark_hostside/java/android/avf/test/AVFHostTestCase.java
@@ -19,6 +19,7 @@
import static com.android.tradefed.device.TestDevice.MicrodroidBuilder;
import static com.android.tradefed.testtype.DeviceJUnit4ClassRunner.TestMetrics;
+import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static com.google.common.truth.TruthJUnit.assume;
@@ -113,16 +114,78 @@
@Test
public void testBootWithCompOS() throws Exception {
- composTestHelper(true);
+ composTestHelper(true, "microdroid");
+ }
+
+ @Test
+ public void testBootWithCompOS_os_android15_66() throws Exception {
+ composTestHelper(true, "android15_66");
+ }
+
+ @Test
+ public void testBootWithCompOS_os_microdroid_16k() throws Exception {
+ composTestHelper(true, "microdroid_16k");
}
@Test
public void testBootWithoutCompOS() throws Exception {
- composTestHelper(false);
+ composTestHelper(false, null);
}
@Test
public void testNoLongHypSections() throws Exception {
+ noLongHypSectionsHelper("microdroid");
+ }
+
+ @Test
+ public void testNoLongHypSections_os_android15_66() throws Exception {
+ noLongHypSectionsHelper("android15_66");
+ }
+
+ @Test
+ public void testNoLongHypSections_os_microdroid_16k() throws Exception {
+ noLongHypSectionsHelper("microdroid_16k");
+ }
+
+ @Test
+ public void testPsciMemProtect() throws Exception {
+ psciMemProtectHelper("microdroid");
+ }
+
+ @Test
+ public void testPsciMemProtect_os_android15_66() throws Exception {
+ psciMemProtectHelper("android15_66");
+ }
+
+ @Test
+ public void testPsciMemProtect_os_microdroid_16k() throws Exception {
+ psciMemProtectHelper("microdroid_16k");
+ }
+
+ @Test
+ public void testCameraAppStartupTime() throws Exception {
+ String[] launchIntentPackages = {
+ "com.android.camera2",
+ "com.google.android.GoogleCamera/com.android.camera.CameraLauncher"
+ };
+ String launchIntentPackage = findSupportedPackage(launchIntentPackages);
+ assume().withMessage("No supported camera package").that(launchIntentPackage).isNotNull();
+ appStartupHelper(launchIntentPackage);
+ }
+
+ @Test
+ public void testSettingsAppStartupTime() throws Exception {
+ String[] launchIntentPackages = {"com.android.settings"};
+ String launchIntentPackage = findSupportedPackage(launchIntentPackages);
+ assume().withMessage("No supported settings package").that(launchIntentPackage).isNotNull();
+ appStartupHelper(launchIntentPackage);
+ }
+
+ private void noLongHypSectionsHelper(String osKey) throws Exception {
+ assumeKernelSupported(osKey);
+ assumeVmTypeSupported(osKey, true);
+ String os = SUPPORTED_OSES.get(osKey);
+
String[] hypEvents = {"hyp_enter", "hyp_exit"};
assumeTrue(
@@ -130,7 +193,7 @@
KvmHypTracer.isSupported(getDevice(), hypEvents));
KvmHypTracer tracer = new KvmHypTracer(getDevice(), hypEvents);
- String result = tracer.run(COMPOSD_CMD_BIN + " test-compile");
+ String result = tracer.run(COMPOSD_CMD_BIN + " test-compile --os " + os);
assertWithMessage("Failed to test compilation VM.")
.that(result)
.ignoringCase()
@@ -141,8 +204,11 @@
CLog.i("Hypervisor traces parsed successfully.");
}
- @Test
- public void testPsciMemProtect() throws Exception {
+ public void psciMemProtectHelper(String osKey) throws Exception {
+ assumeKernelSupported(osKey);
+ assumeVmTypeSupported(osKey, true);
+ String os = SUPPORTED_OSES.get(osKey);
+
String[] hypEvents = {"psci_mem_protect"};
assumeTrue(
@@ -151,7 +217,12 @@
KvmHypTracer tracer = new KvmHypTracer(getDevice(), hypEvents);
/* We need to wait for crosvm to die so all the VM pages are reclaimed */
- String result = tracer.run(COMPOSD_CMD_BIN + " test-compile && killall -w crosvm || true");
+ String result =
+ tracer.run(
+ COMPOSD_CMD_BIN
+ + " test-compile --os "
+ + os
+ + " && killall -w crosvm || true");
assertWithMessage("Failed to test compilation VM.")
.that(result)
.ignoringCase()
@@ -176,25 +247,6 @@
.isGreaterThan(0);
}
- @Test
- public void testCameraAppStartupTime() throws Exception {
- String[] launchIntentPackages = {
- "com.android.camera2",
- "com.google.android.GoogleCamera/com.android.camera.CameraLauncher"
- };
- String launchIntentPackage = findSupportedPackage(launchIntentPackages);
- assume().withMessage("No supported camera package").that(launchIntentPackage).isNotNull();
- appStartupHelper(launchIntentPackage);
- }
-
- @Test
- public void testSettingsAppStartupTime() throws Exception {
- String[] launchIntentPackages = {"com.android.settings"};
- String launchIntentPackage = findSupportedPackage(launchIntentPackages);
- assume().withMessage("No supported settings package").that(launchIntentPackage).isNotNull();
- appStartupHelper(launchIntentPackage);
- }
-
private void appStartupHelper(String launchIntentPackage) throws Exception {
assumeTrue(
"Skip on non-protected VMs",
@@ -471,8 +523,14 @@
throw new IllegalArgumentException("Failed to get boot time info.");
}
- private void composTestHelper(boolean isWithCompos) throws Exception {
+ private void composTestHelper(boolean isWithCompos, String osKey) throws Exception {
assumeFalse("Skip on CF; too slow", isCuttlefish());
+ if (isWithCompos) {
+ assumeKernelSupported(osKey);
+ assumeVmTypeSupported(osKey, true);
+ } else {
+ assertThat(osKey).isNull();
+ }
List<Double> bootDmesgTime = new ArrayList<>(ROUND_COUNT);
@@ -480,7 +538,8 @@
reInstallApex(REINSTALL_APEX_TIMEOUT_SEC);
try {
if (isWithCompos) {
- compileStagedApex(COMPILE_STAGED_APEX_TIMEOUT_SEC);
+ String os = SUPPORTED_OSES.get(osKey);
+ compileStagedApex(COMPILE_STAGED_APEX_TIMEOUT_SEC, os);
}
} finally {
// If compilation fails, we still have a staged APEX, and we need to reboot to
@@ -518,7 +577,7 @@
getDevice().enableAdbRoot();
}
- private void compileStagedApex(int timeoutSec) throws Exception {
+ private void compileStagedApex(int timeoutSec, String os) throws Exception {
long timeStart = System.currentTimeMillis();
long timeEnd = timeStart + timeoutSec * 1000L;
@@ -530,7 +589,7 @@
String result =
android.runWithTimeout(
- 3 * 60 * 1000, COMPOSD_CMD_BIN + " staged-apex-compile");
+ 3 * 60 * 1000, COMPOSD_CMD_BIN + " staged-apex-compile --os " + os);
assertWithMessage("Failed to compile staged APEX. Reason: " + result)
.that(result)
.ignoringCase()
diff --git a/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java b/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java
index 67249b4..18ed7b6 100644
--- a/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java
+++ b/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java
@@ -58,9 +58,10 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
-import java.util.Arrays;
+import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
+import java.util.List;
import java.util.OptionalLong;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
@@ -73,13 +74,21 @@
private final String MAX_PERFORMANCE_TASK_PROFILE = "CPUSET_SP_TOP_APP";
protected static final String KERNEL_VERSION = SystemProperties.get("ro.kernel.version");
+
+ private static final List<String> getSupportedOSes() {
+ List<String> ret = new ArrayList<>();
+ ret.add("microdroid");
+ if (Build.VERSION.SDK_INT >= 35) {
+ ret.add("microdroid_gki-android15-6.6");
+ }
+ if (Build.VERSION.SDK_INT >= 36) {
+ ret.add("microdroid_16k");
+ }
+ return ret;
+ }
+
protected static final Set<String> SUPPORTED_OSES =
- Collections.unmodifiableSet(
- new HashSet<>(
- Arrays.asList(
- "microdroid",
- "microdroid_16k",
- "microdroid_gki-android15-6.6")));
+ Collections.unmodifiableSet(new HashSet<>(getSupportedOSes()));
private static final long ONE_MEBI = 1024 * 1024;
private static final long MIN_MEM_ARM64 = 170 * ONE_MEBI;
@@ -155,10 +164,12 @@
}
public VirtualMachineConfig.Builder newVmConfigBuilderWithPayloadConfig(String configPath) {
- return new VirtualMachineConfig.Builder(mCtx)
- .setProtectedVm(mProtectedVm)
- .setOs(os())
- .setPayloadConfigPath(configPath);
+ VirtualMachineConfig.Builder builder = new VirtualMachineConfig.Builder(mCtx);
+ builder.setProtectedVm(mProtectedVm).setPayloadConfigPath(configPath);
+ if (Build.VERSION.SDK_INT >= 35) {
+ builder.setOs(os());
+ }
+ return builder;
}
public VirtualMachineConfig.Builder newVmConfigBuilderWithPayloadBinary(String binaryPath) {
@@ -288,15 +299,18 @@
}
protected boolean isUpdatableVmSupported() throws VirtualMachineException {
- return getVirtualMachineManager().isUpdatableVmSupported();
+ // Pre-36 OS doesn't have VirtualMachineManager#isUpdatableVmSupported.
+ if (Build.VERSION.SDK_INT >= 35) {
+ return getVirtualMachineManager().isUpdatableVmSupported();
+ }
+ return false;
}
protected void ensureVmAttestationSupported() throws Exception {
// The first vendor API level is checked because VM attestation requires the VM DICE chain
// to be ROM-rooted.
int firstVendorApiLevel = getFirstVendorApiLevel();
- boolean isRemoteAttestationSupported =
- getVirtualMachineManager().isRemoteAttestationSupported();
+ boolean isRemoteAttestationSupported = isRemoteAttestationSupported();
if (firstVendorApiLevel >= 202504) {
assertWithMessage(
"First vendor API '"
@@ -309,6 +323,14 @@
}
}
+ protected boolean isRemoteAttestationSupported() throws VirtualMachineException {
+ // Pre-36 OS doesn't have VirtualMachineManager#isRemoteAttestionSupported
+ if (Build.VERSION.SDK_INT >= 35) {
+ return getVirtualMachineManager().isRemoteAttestationSupported();
+ }
+ return false;
+ }
+
public abstract static class VmEventListener implements VirtualMachineCallback {
private ExecutorService mExecutorService = Executors.newSingleThreadExecutor();
private OptionalLong mVcpuStartedNanoTime = OptionalLong.empty();
diff --git a/tests/hostside/helper/java/com/android/microdroid/test/host/MicrodroidHostTestCaseBase.java b/tests/hostside/helper/java/com/android/microdroid/test/host/MicrodroidHostTestCaseBase.java
index ad37dda..fcef19a 100644
--- a/tests/hostside/helper/java/com/android/microdroid/test/host/MicrodroidHostTestCaseBase.java
+++ b/tests/hostside/helper/java/com/android/microdroid/test/host/MicrodroidHostTestCaseBase.java
@@ -18,6 +18,7 @@
import static com.android.tradefed.testtype.DeviceJUnit4ClassRunner.TestLogData;
+import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static org.junit.Assume.assumeFalse;
@@ -267,4 +268,27 @@
protected boolean isPkvmHypervisor() throws DeviceNotAvailableException {
return "kvm.arm-protected".equals(getDevice().getProperty("ro.boot.hypervisor.version"));
}
+
+ protected TestDevice getAndroidDevice() {
+ TestDevice androidDevice = (TestDevice) getDevice();
+ assertThat(androidDevice).isNotNull();
+ return androidDevice;
+ }
+
+ protected void assumeKernelSupported(String osKey) throws Exception {
+ String os = SUPPORTED_OSES.get(osKey);
+ assumeTrue(
+ "Skipping test as OS \"" + os + "\" is not supported",
+ getSupportedOSList().contains(os));
+ }
+
+ protected void assumeVmTypeSupported(String os, boolean protectedVm) throws Exception {
+ // TODO(b/376870129): remove this check
+ if (protectedVm) {
+ assumeFalse("pVMs with 16k kernel are not supported yet :(", os.endsWith("_16k"));
+ }
+ assumeTrue(
+ "Microdroid is not supported for specific VM protection type",
+ getAndroidDevice().supportsMicrodroid(protectedVm));
+ }
}
diff --git a/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java b/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
index 7864f3f..59a57f1 100644
--- a/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
+++ b/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
@@ -1474,12 +1474,6 @@
}
}
- private TestDevice getAndroidDevice() {
- TestDevice androidDevice = (TestDevice) getDevice();
- assertThat(androidDevice).isNotNull();
- return androidDevice;
- }
-
// The TradeFed Dockerfile sets LD_LIBRARY_PATH to a directory with an older libc++.so, which
// breaks binaries that are linked against a newer libc++.so. Binaries commonly use DT_RUNPATH
// to find an adjacent libc++.so (e.g. `$ORIGIN/../lib64`), but LD_LIBRARY_PATH overrides
@@ -1490,23 +1484,6 @@
return runUtil;
}
- private void assumeKernelSupported(String osKey) throws Exception {
- String os = SUPPORTED_OSES.get(osKey);
- assumeTrue(
- "Skipping test as OS \"" + os + "\" is not supported",
- getSupportedOSList().contains(os));
- }
-
- private void assumeVmTypeSupported(String os, boolean protectedVm) throws Exception {
- // TODO(b/376870129): remove this check
- if (protectedVm) {
- assumeFalse("pVMs with 16k kernel are not supported yet :(", os.endsWith("_16k"));
- }
- assumeTrue(
- "Microdroid is not supported for specific VM protection type",
- getAndroidDevice().supportsMicrodroid(protectedVm));
- }
-
private void assumeArm64Supported() throws Exception {
CommandRunner android = new CommandRunner(getDevice());
String abi = android.run("getprop", "ro.product.cpu.abi");
diff --git a/tests/testapk/Android.bp b/tests/testapk/Android.bp
index d9f74dc..cc0015d 100644
--- a/tests/testapk/Android.bp
+++ b/tests/testapk/Android.bp
@@ -28,6 +28,9 @@
use_embedded_native_libs: true,
// We only support 64-bit ABI, but CTS demands all APKs to be multi-ABI.
compile_multilib: "both",
+ lint: {
+ error_checks: ["NewApi"],
+ },
}
java_defaults {
diff --git a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
index e4a3ff6..639224d 100644
--- a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
+++ b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
@@ -257,7 +257,7 @@
assumeProtectedVM();
assume().withMessage(
"This test does not apply to a device that supports Remote Attestation")
- .that(getVirtualMachineManager().isRemoteAttestationSupported())
+ .that(isRemoteAttestationSupported())
.isFalse();
VirtualMachineConfig config =
newVmConfigBuilderWithPayloadBinary(VM_ATTESTATION_PAYLOAD_PATH)
@@ -285,7 +285,7 @@
// pVM remote attestation is only supported on protected VMs.
assumeProtectedVM();
assume().withMessage("Test needs Remote Attestation support")
- .that(getVirtualMachineManager().isRemoteAttestationSupported())
+ .that(isRemoteAttestationSupported())
.isTrue();
File vendorDiskImage = new File("/vendor/etc/avf/microdroid/microdroid_vendor.img");
assumeTrue("Microdroid vendor image doesn't exist, skip", vendorDiskImage.exists());