add modifier keys in terminal app
Except ctrl key, it just generates a matched keycode to the view
ctrl key, it enables javascript variable `ctrl`, and handler inside
xtermjs reads ctrl variable and the keycode, and if it is convertible
to ctrl keycode, it does.
Test: test added keys
Bug: 376813452
Change-Id: I22e5f80ca75ffe6426b7374855cdd015ad08f324
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java
index 8448349..4092c60 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java
@@ -70,6 +70,7 @@
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
+import java.util.Map;
public class MainActivity extends BaseActivity
implements VmLauncherServices.VmLauncherServiceCallback, AccessibilityStateChangeListener {
@@ -87,6 +88,20 @@
private static final int POST_NOTIFICATIONS_PERMISSION_REQUEST_CODE = 101;
private ActivityResultLauncher<Intent> mManageExternalStorageActivityResultLauncher;
private static int diskSizeStep;
+ private static final Map<Integer, Integer> BTN_KEY_CODE_MAP =
+ Map.ofEntries(
+ Map.entry(R.id.btn_tab, KeyEvent.KEYCODE_TAB),
+ // Alt key sends ESC keycode
+ Map.entry(R.id.btn_alt, KeyEvent.KEYCODE_ESCAPE),
+ Map.entry(R.id.btn_esc, KeyEvent.KEYCODE_ESCAPE),
+ Map.entry(R.id.btn_left, KeyEvent.KEYCODE_DPAD_LEFT),
+ Map.entry(R.id.btn_right, KeyEvent.KEYCODE_DPAD_RIGHT),
+ Map.entry(R.id.btn_up, KeyEvent.KEYCODE_DPAD_UP),
+ Map.entry(R.id.btn_down, KeyEvent.KEYCODE_DPAD_DOWN),
+ Map.entry(R.id.btn_home, KeyEvent.KEYCODE_MOVE_HOME),
+ Map.entry(R.id.btn_end, KeyEvent.KEYCODE_MOVE_END),
+ Map.entry(R.id.btn_pgup, KeyEvent.KEYCODE_PAGE_UP),
+ Map.entry(R.id.btn_pgdn, KeyEvent.KEYCODE_PAGE_DOWN));
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -116,6 +131,8 @@
mWebView.getSettings().setJavaScriptEnabled(true);
mWebView.setWebChromeClient(new WebChromeClient());
+ setupModifierKeys();
+
mAccessibilityManager = getSystemService(AccessibilityManager.class);
mAccessibilityManager.addAccessibilityStateChangeListener(this);
@@ -139,6 +156,32 @@
}
}
+ private void setupModifierKeys() {
+ // Only ctrl key is special, it communicates with xtermjs to modify key event with ctrl key
+ findViewById(R.id.btn_ctrl)
+ .setOnClickListener(
+ (v) -> {
+ mWebView.loadUrl(TerminalView.CTRL_KEY_HANDLER);
+ mWebView.loadUrl(TerminalView.ENABLE_CTRL_KEY);
+ });
+
+ View.OnClickListener modifierButtonClickListener =
+ v -> {
+ if (BTN_KEY_CODE_MAP.containsKey(v.getId())) {
+ int keyCode = BTN_KEY_CODE_MAP.get(v.getId());
+ mWebView.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, keyCode));
+ mWebView.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, keyCode));
+ }
+ };
+
+ for (int btn : BTN_KEY_CODE_MAP.keySet()) {
+ View v = findViewById(btn);
+ if (v != null) {
+ v.setOnClickListener(modifierButtonClickListener);
+ }
+ }
+ }
+
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
if (Build.isDebuggable() && event.getKeyCode() == KeyEvent.KEYCODE_UNKNOWN) {
@@ -240,8 +283,16 @@
android.os.Trace.endAsyncSection("executeTerminal", 0);
findViewById(R.id.boot_progress)
.setVisibility(View.GONE);
- view.setVisibility(View.VISIBLE);
+ findViewById(R.id.webview_container)
+ .setVisibility(View.VISIBLE);
mBootCompleted.open();
+ // TODO(b/376813452): support talkback as well
+ int keyVisibility =
+ mAccessibilityManager.isEnabled()
+ ? View.GONE
+ : View.VISIBLE;
+ findViewById(R.id.keyboard_container)
+ .setVisibility(keyVisibility);
}
}
});
@@ -411,6 +462,7 @@
@Override
public void onAccessibilityStateChanged(boolean enabled) {
+ findViewById(R.id.keyboard_container).setVisibility(enabled ? View.GONE : View.VISIBLE);
connectToTerminalService();
}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/TerminalView.java b/android/TerminalApp/java/com/android/virtualization/terminal/TerminalView.java
index aabcae7..b83a1ca 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/TerminalView.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/TerminalView.java
@@ -43,6 +43,39 @@
// arbitrarily set. We may want to adjust this in the future.
private static final int TEXT_TOO_LONG_TO_ANNOUNCE = 200;
+ // keyCode 229 means composing text, so get the last character in e.target.value.
+ // keycode 64(@)-95(_) is mapped to a ctrl code
+ // keycode 97(A)-122(Z) is converted to a small letter, and mapped to ctrl code
+ public static final String CTRL_KEY_HANDLER =
+ """
+javascript: (function() {
+ window.term.attachCustomKeyEventHandler((e) => {
+ if (window.ctrl) {
+ keyCode = e.keyCode;
+ if (keyCode === 229) {
+ keyCode = e.target.value.charAt(e.target.selectionStart - 1).charCodeAt();
+ }
+ if (64 <= keyCode && keyCode <= 95) {
+ input = String.fromCharCode(keyCode - 64);
+ } else if (97 <= keyCode && keyCode <= 122) {
+ input = String.fromCharCode(keyCode - 96);
+ } else {
+ return true;
+ }
+ if (e.type === 'keyup') {
+ window.term.input(input);
+ e.target.value = e.target.value.slice(0, -1);
+ window.ctrl = false;
+ }
+ return false;
+ } else {
+ return true;
+ }
+ });
+})();
+""";
+ public static final String ENABLE_CTRL_KEY = "javascript:(function(){window.ctrl=true;})();";
+
private final AccessibilityManager mA11yManager;
public TerminalView(Context context, AttributeSet attrs) {
diff --git a/android/TerminalApp/res/layout/activity_headless.xml b/android/TerminalApp/res/layout/activity_headless.xml
index 3b01179..0bcfbea 100644
--- a/android/TerminalApp/res/layout/activity_headless.xml
+++ b/android/TerminalApp/res/layout/activity_headless.xml
@@ -47,13 +47,20 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
- <com.android.virtualization.terminal.TerminalView
- android:id="@+id/webview"
- android:layout_marginBottom="5dp"
- android:layout_gravity="fill"
+ <LinearLayout
+ android:id="@+id/webview_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:visibility="invisible"/>
+ 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" />
+ <include layout="@layout/layout_keyboard" />
+ </LinearLayout>
</FrameLayout>
</LinearLayout>
diff --git a/android/TerminalApp/res/layout/layout_keyboard.xml b/android/TerminalApp/res/layout/layout_keyboard.xml
new file mode 100644
index 0000000..d8b7e11
--- /dev/null
+++ b/android/TerminalApp/res/layout/layout_keyboard.xml
@@ -0,0 +1,82 @@
+<?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/keyboard_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical" >
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+ <Button
+ style="@style/ModifierKeyStyle"
+ android:id="@+id/btn_esc"
+ android:text="@string/btn_esc_text" />
+ <Button
+ style="@style/ModifierKeyStyle"
+ android:id="@+id/btn_tab"
+ android:text="@string/btn_tab_text" />
+ <Button
+ style="@style/ModifierKeyStyle"
+ android:id="@+id/btn_home"
+ android:text="@string/btn_home_text" />
+ <Button
+ style="@style/ModifierKeyStyle"
+ android:id="@+id/btn_up"
+ android:text="@string/btn_up_text" />
+ <Button
+ style="@style/ModifierKeyStyle"
+ android:id="@+id/btn_end"
+ android:text="@string/btn_end_text" />
+ <Button
+ style="@style/ModifierKeyStyle"
+ android:id="@+id/btn_pgup"
+ android:text="@string/btn_pgup_text" />
+ </LinearLayout>
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+ <Button
+ style="@style/ModifierKeyStyle"
+ android:id="@+id/btn_ctrl"
+ android:text="@string/btn_ctrl_text" />
+ <Button
+ style="@style/ModifierKeyStyle"
+ android:id="@+id/btn_alt"
+ android:text="@string/btn_alt_text" />
+ <Button
+ style="@style/ModifierKeyStyle"
+ android:id="@+id/btn_left"
+ android:text="@string/btn_left_text" />
+ <Button
+ style="@style/ModifierKeyStyle"
+ android:id="@+id/btn_down"
+ android:text="@string/btn_down_text" />
+ <Button
+ style="@style/ModifierKeyStyle"
+ android:id="@+id/btn_right"
+ android:text="@string/btn_right_text" />
+ <Button
+ style="@style/ModifierKeyStyle"
+ android:id="@+id/btn_pgdn"
+ android:text="@string/btn_pgdn_text" />
+ </LinearLayout>
+</LinearLayout>
\ No newline at end of file
diff --git a/android/TerminalApp/res/values/keyboard_btn_strings.xml b/android/TerminalApp/res/values/keyboard_btn_strings.xml
new file mode 100644
index 0000000..384c583
--- /dev/null
+++ b/android/TerminalApp/res/values/keyboard_btn_strings.xml
@@ -0,0 +1,31 @@
+<?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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="btn_esc_text" translatable="false">Esc</string>
+ <string name="btn_tab_text" translatable="false">Tab</string>
+ <string name="btn_home_text" translatable="false">Home</string>
+ <string name="btn_up_text" translatable="false">↑</string>
+ <string name="btn_end_text" translatable="false">End</string>
+ <string name="btn_pgup_text" translatable="false">PgUp</string>
+ <string name="btn_ctrl_text" translatable="false">Ctrl</string>
+ <string name="btn_alt_text" translatable="false">Alt</string>
+ <string name="btn_left_text" translatable="false">←</string>
+ <string name="btn_down_text" translatable="false">↓</string>
+ <string name="btn_right_text" translatable="false">→</string>
+ <string name="btn_pgdn_text" translatable="false">PgDn</string>
+</resources>
diff --git a/android/TerminalApp/res/values/styles.xml b/android/TerminalApp/res/values/styles.xml
new file mode 100644
index 0000000..ee80862
--- /dev/null
+++ b/android/TerminalApp/res/values/styles.xml
@@ -0,0 +1,27 @@
+<?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.
+-->
+<resources>
+ <style name="ModifierKeyStyle" parent="@style/Widget.Material3.Button.TextButton">
+ <item name="android:textAppearance">?android:attr/textAppearanceSmall</item>
+ <item name="android:textColor">?android:attr/textColorPrimary</item>
+ <item name="android:layout_weight">1</item>
+ <item name="android:layout_width">0dp</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:paddingHorizontal">0dp</item>
+ <item name="android:hapticFeedbackEnabled">true</item>
+ </style>
+</resources>
\ No newline at end of file