Introduce JavascriptAddon

Bug: 375326606
Test: long press to select && ^C
Change-Id: If292e92b4e0c8d3626b8357d81becea39906ef7c
diff --git a/android/TerminalApp/.gitignore b/android/TerminalApp/.gitignore
index e81da29..e69de29 100644
--- a/android/TerminalApp/.gitignore
+++ b/android/TerminalApp/.gitignore
@@ -1,2 +0,0 @@
-assets/*
-!assets/.gitkeep
diff --git a/android/TerminalApp/Android.bp b/android/TerminalApp/Android.bp
index 733a72b..4bb9703 100644
--- a/android/TerminalApp/Android.bp
+++ b/android/TerminalApp/Android.bp
@@ -8,6 +8,7 @@
         "java/**/*.java",
         "java/**/*.kt",
     ],
+    asset_dirs: ["assets"],
     resource_dirs: ["res"],
     static_libs: [
         "androidx-constraintlayout_constraintlayout",
diff --git a/android/TerminalApp/assets/js/ctrl_key_handler.js b/android/TerminalApp/assets/js/ctrl_key_handler.js
new file mode 100644
index 0000000..de901fc
--- /dev/null
+++ b/android/TerminalApp/assets/js/ctrl_key_handler.js
@@ -0,0 +1,45 @@
+/*
+ * 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.
+ */
+
+(function() {
+// 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
+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;
+  }
+});
+})();
\ No newline at end of file
diff --git a/android/TerminalApp/assets/js/enable_ctrl_key.js b/android/TerminalApp/assets/js/enable_ctrl_key.js
new file mode 100644
index 0000000..4aedcfe
--- /dev/null
+++ b/android/TerminalApp/assets/js/enable_ctrl_key.js
@@ -0,0 +1,19 @@
+/*
+ * 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.
+ */
+
+(function() {
+window.ctrl = true;
+})();
\ No newline at end of file
diff --git a/android/TerminalApp/assets/js/touch_to_mouse_handler.js b/android/TerminalApp/assets/js/touch_to_mouse_handler.js
new file mode 100644
index 0000000..fce03d6
--- /dev/null
+++ b/android/TerminalApp/assets/js/touch_to_mouse_handler.js
@@ -0,0 +1,79 @@
+/*
+ * 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.
+ */
+
+(function() {
+// TODO(b/375326606): consider contribution on
+// upstream(https://github.com/xtermjs/xterm.js/issues/3727)
+let convertTouchToMouse = false;
+function touchHandler(event) {
+  const contextmenuByTouch =
+      event.type === 'contextmenu' && event.pointerType === 'touch';
+  // Only proceed for long touches (contextmenu) or when converting touch to
+  // mouse
+  if (!contextmenuByTouch && !convertTouchToMouse) {
+    return;
+  }
+
+  const touch = event.changedTouches ? event.changedTouches[0] : event;
+
+  let type;
+  switch (event.type) {
+    case 'contextmenu':
+      convertTouchToMouse = true;
+      type = 'mousedown';
+      break;
+    case 'touchmove':
+      type = 'mousemove';
+      break;
+    case 'touchend':
+      convertTouchToMouse = false;
+      type = 'mouseup';
+      break;
+    default:
+      convertTouchToMouse = false;
+      return;
+  }
+
+  const simulatedEvent = new MouseEvent(type, {
+    bubbles: true,
+    cancelable: true,
+    view: window,
+    detail: 1,
+    screenX: touch.screenX,
+    screenY: touch.screenY,
+    clientX: touch.clientX,
+    clientY: touch.clientY,
+    button: 0,  // left click
+  });
+
+  touch.target.dispatchEvent(simulatedEvent);
+
+  // Prevent default behavior for touch events (except contextmenu)
+  if (event.type !== 'contextmenu') {
+    event.preventDefault();
+    event.stopPropagation();
+  }
+}
+const eventOptions = {
+  capture: true,
+  passive: false
+};
+document.addEventListener('touchstart', touchHandler, eventOptions);
+document.addEventListener('touchmove', touchHandler, eventOptions);
+document.addEventListener('touchend', touchHandler, eventOptions);
+document.addEventListener('touchcancel', touchHandler, eventOptions);
+document.addEventListener('contextmenu', touchHandler, eventOptions);
+})();
\ No newline at end of file
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java
index bfa425d..cfb1cbe 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java
@@ -80,7 +80,7 @@
     private InstalledImage mImage;
     private X509Certificate[] mCertificates;
     private PrivateKey mPrivateKey;
-    private WebView mWebView;
+    private TerminalView mTerminalView;
     private AccessibilityManager mAccessibilityManager;
     private ConditionVariable mBootCompleted = new ConditionVariable();
     private static final int POST_NOTIFICATIONS_PERMISSION_REQUEST_CODE = 101;
@@ -113,12 +113,12 @@
 
         MaterialToolbar toolbar = (MaterialToolbar) findViewById(R.id.toolbar);
         setSupportActionBar(toolbar);
-        mWebView = (WebView) findViewById(R.id.webview);
-        mWebView.getSettings().setDatabaseEnabled(true);
-        mWebView.getSettings().setDomStorageEnabled(true);
-        mWebView.getSettings().setJavaScriptEnabled(true);
-        mWebView.getSettings().setCacheMode(LOAD_NO_CACHE);
-        mWebView.setWebChromeClient(new WebChromeClient());
+        mTerminalView = (TerminalView) findViewById(R.id.webview);
+        mTerminalView.getSettings().setDatabaseEnabled(true);
+        mTerminalView.getSettings().setDomStorageEnabled(true);
+        mTerminalView.getSettings().setJavaScriptEnabled(true);
+        mTerminalView.getSettings().setCacheMode(LOAD_NO_CACHE);
+        mTerminalView.setWebChromeClient(new WebChromeClient());
 
         setupModifierKeys();
 
@@ -173,16 +173,16 @@
         findViewById(R.id.btn_ctrl)
                 .setOnClickListener(
                         (v) -> {
-                            mWebView.evaluateJavascript(TerminalView.CTRL_KEY_HANDLER, null);
-                            mWebView.evaluateJavascript(TerminalView.ENABLE_CTRL_KEY, null);
+                            mTerminalView.mapCtrlKey();
+                            mTerminalView.enableCtrlKey();
                         });
 
         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));
+                        mTerminalView.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, keyCode));
+                        mTerminalView.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, keyCode));
                     }
                 };
 
@@ -246,7 +246,7 @@
 
     private void connectToTerminalService() {
         Log.i(TAG, "URL=" + getTerminalServiceUrl().toString());
-        mWebView.setWebViewClient(
+        mTerminalView.setWebViewClient(
                 new WebViewClient() {
                     private boolean mLoadFailed = false;
                     private long mRequestId = 0;
@@ -300,8 +300,7 @@
                                                     .setVisibility(View.VISIBLE);
                                             mBootCompleted.open();
                                             updateModifierKeysVisibility();
-                                            mWebView.evaluateJavascript(
-                                                    TerminalView.TOUCH_TO_MOUSE_HANDLER, null);
+                                            mTerminalView.mapTouchToMouseEvent();
                                         }
                                     }
                                 });
@@ -328,7 +327,9 @@
                         () -> {
                             waitUntilVmStarts();
                             runOnUiThread(
-                                    () -> mWebView.loadUrl(getTerminalServiceUrl().toString()));
+                                    () ->
+                                            mTerminalView.loadUrl(
+                                                    getTerminalServiceUrl().toString()));
                         })
                 .start();
     }
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/TerminalView.java b/android/TerminalApp/java/com/android/virtualization/terminal/TerminalView.java
index 3f09e35..c57c4c0 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/TerminalView.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/TerminalView.java
@@ -38,6 +38,8 @@
 import android.view.inputmethod.InputConnection;
 import android.webkit.WebView;
 
+import java.io.IOException;
+import java.io.InputStream;
 import java.util.List;
 
 public class TerminalView extends WebView
@@ -46,105 +48,9 @@
     // 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 =
-            """
-(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 = "(function(){window.ctrl=true;})();";
-
-    // TODO(b/375326606): consider contribution on
-    // upstream(https://github.com/xtermjs/xterm.js/issues/3727)
-    public static final String TOUCH_TO_MOUSE_HANDLER =
-            """
-(function() {
-let convertTouchToMouse = false;
-function touchHandler(event) {
-  const contextmenuByTouch =
-      event.type === 'contextmenu' && event.pointerType === 'touch';
-  // Only proceed for long touches (contextmenu) or when converting touch to
-  // mouse
-  if (!contextmenuByTouch && !convertTouchToMouse) {
-    return;
-  }
-
-  const touch = event.changedTouches ? event.changedTouches[0] : event;
-
-  let type;
-  switch (event.type) {
-    case 'contextmenu':
-      convertTouchToMouse = true;
-      type = 'mousedown';
-      break;
-    case 'touchmove':
-      type = 'mousemove';
-      break;
-    case 'touchend':
-      convertTouchToMouse = false;
-      type = 'mouseup';
-      break;
-    default:
-      convertTouchToMouse = false;
-      return;
-  }
-
-  const simulatedEvent = new MouseEvent(type, {
-    bubbles: true,
-    cancelable: true,
-    view: window,
-    detail: 1,
-    screenX: touch.screenX,
-    screenY: touch.screenY,
-    clientX: touch.clientX,
-    clientY: touch.clientY,
-    button: 0,  // left click
-  });
-
-  touch.target.dispatchEvent(simulatedEvent);
-
-  // Prevent default behavior for touch events (except contextmenu)
-  if (event.type !== 'contextmenu') {
-    event.preventDefault();
-    event.stopPropagation();
-  }
-}
-const eventOptions = {
-  capture: true,
-  passive: false
-};
-document.addEventListener('touchstart', touchHandler, eventOptions);
-document.addEventListener('touchmove', touchHandler, eventOptions);
-document.addEventListener('touchend', touchHandler, eventOptions);
-document.addEventListener('touchcancel', touchHandler, eventOptions);
-document.addEventListener('contextmenu', touchHandler, eventOptions);
-})();
-""";
+    private final String CTRL_KEY_HANDLER;
+    private final String ENABLE_CTRL_KEY;
+    private final String TOUCH_TO_MOUSE_HANDLER;
 
     private final AccessibilityManager mA11yManager;
 
@@ -155,6 +61,32 @@
         mA11yManager.addTouchExplorationStateChangeListener(this);
         mA11yManager.addAccessibilityStateChangeListener(this);
         adjustToA11yStateChange();
+        try {
+            CTRL_KEY_HANDLER = readAssetAsString(context, "js/ctrl_key_handler.js");
+            ENABLE_CTRL_KEY = readAssetAsString(context, "js/enable_ctrl_key.js");
+            TOUCH_TO_MOUSE_HANDLER = readAssetAsString(context, "js/touch_to_mouse_handler.js");
+        } catch (IOException e) {
+            // It cannot happen
+            throw new IllegalArgumentException("cannot read code from asset", e);
+        }
+    }
+
+    private String readAssetAsString(Context context, String filePath) throws IOException {
+        try (InputStream is = context.getAssets().open(filePath)) {
+            return new String(is.readAllBytes());
+        }
+    }
+
+    public void mapTouchToMouseEvent() {
+        this.evaluateJavascript(TOUCH_TO_MOUSE_HANDLER, null);
+    }
+
+    public void mapCtrlKey() {
+        this.evaluateJavascript(CTRL_KEY_HANDLER, null);
+    }
+
+    public void enableCtrlKey() {
+        this.evaluateJavascript(ENABLE_CTRL_KEY, null);
     }
 
     @Override