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