Add ModifierKeysController
ModifierKeysContoller abstracts the handling of the modifier keys.
This change also adds the support for single-line modifier keys which is
used when the height of the terminal is too short.
Bug: 385251193
Test: show keyboard in split screen landscape mode
Change-Id: I7eb7333a28cdc0453b1e30a64e5d215d05ceb62e
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.kt b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.kt
index 5e039d9..bf2f573 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.kt
@@ -40,7 +40,7 @@
import android.view.Menu
import android.view.MenuItem
import android.view.View
-import android.view.WindowInsets
+import android.view.ViewGroup
import android.view.accessibility.AccessibilityManager
import android.webkit.ClientCertRequest
import android.webkit.SslErrorHandler
@@ -81,10 +81,12 @@
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
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -104,7 +106,9 @@
terminalView.getSettings().setCacheMode(WebSettings.LOAD_NO_CACHE)
terminalView.setWebChromeClient(WebChromeClient())
- setupModifierKeys()
+ terminalContainer = terminalView.parent as ViewGroup
+
+ modifierKeysController = ModifierKeysController(this, terminalView, terminalContainer)
accessibilityManager =
getSystemService<AccessibilityManager>(AccessibilityManager::class.java)
@@ -117,11 +121,6 @@
StartActivityForResult(),
ActivityResultCallback { startVm() },
)
- window.decorView.rootView.setOnApplyWindowInsetsListener { _: View?, insets: WindowInsets ->
- updateModifierKeysVisibility()
- insets
- }
-
executorService =
Executors.newSingleThreadExecutor(TerminalThreadFactory(applicationContext))
@@ -147,30 +146,7 @@
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
lockOrientationIfNecessary()
- updateModifierKeysVisibility()
- }
-
- private fun setupModifierKeys() {
- // Only ctrl key is special, it communicates with xtermjs to modify key event with ctrl key
- findViewById<View>(R.id.btn_ctrl)
- .setOnClickListener(
- View.OnClickListener {
- terminalView.mapCtrlKey()
- terminalView.enableCtrlKey()
- }
- )
-
- val modifierButtonClickListener =
- 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))
- }
- }
-
- for (btn in BTN_KEY_CODE_MAP.keys) {
- findViewById<View>(btn).setOnClickListener(modifierButtonClickListener)
- }
+ modifierKeysController.update()
}
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
@@ -276,10 +252,9 @@
if (completedRequestId == requestId) {
Trace.endAsyncSection("executeTerminal", 0)
findViewById<View?>(R.id.boot_progress).visibility = View.GONE
- findViewById<View?>(R.id.webview_container).visibility =
- View.VISIBLE
+ terminalContainer.visibility = View.VISIBLE
bootCompleted.open()
- updateModifierKeysVisibility()
+ modifierKeysController.update()
terminalView.mapTouchToMouseEvent()
}
}
@@ -379,15 +354,6 @@
connectToTerminalService()
}
- private fun updateModifierKeysVisibility() {
- val imeShown = window.decorView.rootWindowInsets.isVisible(WindowInsets.Type.ime())
- val hasHwQwertyKeyboard = resources.configuration.keyboard == Configuration.KEYBOARD_QWERTY
- val showModifierKeys = imeShown && !hasHwQwertyKeyboard
-
- val modifierKeys = findViewById<View>(R.id.modifier_keys)
- modifierKeys.visibility = if (showModifierKeys) View.VISIBLE else View.GONE
- }
-
private val installerLauncher =
registerForActivityResult(StartActivityForResult()) { result ->
val resultCode = result.resultCode
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/ModifierKeysController.kt b/android/TerminalApp/java/com/android/virtualization/terminal/ModifierKeysController.kt
new file mode 100644
index 0000000..f8f30f9
--- /dev/null
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/ModifierKeysController.kt
@@ -0,0 +1,131 @@
+/*
+ * 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.app.Activity
+import android.content.res.Configuration
+import android.view.KeyEvent
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.view.WindowInsets
+
+class ModifierKeysController(
+ val activity: Activity,
+ val terminalView: TerminalView,
+ val parent: ViewGroup,
+) {
+ private val window = activity.window
+ private val keysSingleLine: View
+ private val keysDoubleLine: View
+
+ 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.
+ 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)
+
+ addClickListeners(keysSingleLine)
+ addClickListeners(keysDoubleLine)
+
+ keysSingleLine.visibility = View.GONE
+ keysDoubleLine.visibility = View.GONE
+
+ // Setup for the update to be called when needed
+ window.decorView.rootView.setOnApplyWindowInsetsListener { _: View?, insets: WindowInsets ->
+ update()
+ insets
+ }
+
+ terminalView.setOnFocusChangeListener { _: View, _: Boolean -> update() }
+ }
+
+ private fun addClickListeners(keys: View) {
+ // Only ctrl key is special, it communicates with xtermjs to modify key event with ctrl key
+ keys
+ .findViewById<View>(R.id.btn_ctrl)
+ .setOnClickListener({
+ terminalView.mapCtrlKey()
+ terminalView.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))
+ }
+ }
+
+ for (btn in BTN_KEY_CODE_MAP.keys) {
+ keys.findViewById<View>(btn).setOnClickListener(listener)
+ }
+ }
+
+ 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)
+ }
+ keysInSingleLine = needSingleLine
+ }
+
+ // 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
+ }
+
+ // 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
+
+ companion object {
+ 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,
+ )
+ }
+}
diff --git a/android/TerminalApp/res/layout/activity_headless.xml b/android/TerminalApp/res/layout/activity_headless.xml
index b4a65cc..e18aa5c 100644
--- a/android/TerminalApp/res/layout/activity_headless.xml
+++ b/android/TerminalApp/res/layout/activity_headless.xml
@@ -48,7 +48,6 @@
android:layout_height="wrap_content"/>
</LinearLayout>
<LinearLayout
- android:id="@+id/webview_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="5dp"
@@ -59,7 +58,6 @@
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
- <include layout="@layout/layout_modifier_keys" />
</LinearLayout>
</FrameLayout>
diff --git a/android/TerminalApp/res/layout/layout_modifier_keys.xml b/android/TerminalApp/res/layout/modifier_keys_doubleline.xml
similarity index 100%
rename from android/TerminalApp/res/layout/layout_modifier_keys.xml
rename to android/TerminalApp/res/layout/modifier_keys_doubleline.xml
diff --git a/android/TerminalApp/res/layout/modifier_keys_singleline.xml b/android/TerminalApp/res/layout/modifier_keys_singleline.xml
new file mode 100644
index 0000000..6ccf48f
--- /dev/null
+++ b/android/TerminalApp/res/layout/modifier_keys_singleline.xml
@@ -0,0 +1,84 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2024 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.
+ -->
+<!--TODO(b/376813452): we might want tablet UI for that-->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/modifier_keys"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal" >
+ <Button
+ style="@style/ModifierKeyStyle"
+ android:id="@+id/btn_esc"
+ android:textSize="10sp"
+ android:text="@string/btn_esc_text" />
+ <Button
+ style="@style/ModifierKeyStyle"
+ android:id="@+id/btn_tab"
+ android:textSize="10sp"
+ android:text="@string/btn_tab_text" />
+ <Button
+ style="@style/ModifierKeyStyle"
+ android:id="@+id/btn_ctrl"
+ android:textSize="10sp"
+ android:text="@string/btn_ctrl_text" />
+ <Button
+ style="@style/ModifierKeyStyle"
+ android:id="@+id/btn_alt"
+ android:textSize="10sp"
+ android:text="@string/btn_alt_text" />
+ <Button
+ style="@style/ModifierKeyStyle"
+ android:id="@+id/btn_home"
+ android:textSize="10sp"
+ android:text="@string/btn_home_text" />
+ <Button
+ style="@style/ModifierKeyStyle"
+ android:id="@+id/btn_end"
+ android:textSize="10sp"
+ android:text="@string/btn_end_text" />
+ <Button
+ style="@style/ModifierKeyStyle"
+ android:id="@+id/btn_left"
+ android:textSize="10sp"
+ android:text="@string/btn_left_text" />
+ <Button
+ style="@style/ModifierKeyStyle"
+ android:id="@+id/btn_down"
+ android:textSize="10sp"
+ android:text="@string/btn_down_text" />
+ <Button
+ style="@style/ModifierKeyStyle"
+ android:id="@+id/btn_up"
+ android:textSize="10sp"
+ android:text="@string/btn_up_text" />
+ <Button
+ style="@style/ModifierKeyStyle"
+ android:id="@+id/btn_right"
+ android:textSize="10sp"
+ android:text="@string/btn_right_text" />
+ <Button
+ style="@style/ModifierKeyStyle"
+ android:id="@+id/btn_pgdn"
+ android:textSize="10sp"
+ android:text="@string/btn_pgdn_text" />
+ <Button
+ style="@style/ModifierKeyStyle"
+ android:id="@+id/btn_pgup"
+ android:textSize="10sp"
+ android:text="@string/btn_pgup_text" />
+</LinearLayout>