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