Snap for 12748980 from aadfce0821e7d1a46399189869729263d7b78f4a to 25Q1-release
Change-Id: I1bbd0aea1b7a96d69004ef72c64ee4f3411bd16b
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/DebianServiceImpl.java b/android/TerminalApp/java/com/android/virtualization/terminal/DebianServiceImpl.java
index 93b0b0c..d167da3 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/DebianServiceImpl.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/DebianServiceImpl.java
@@ -19,7 +19,6 @@
import static com.android.virtualization.terminal.MainActivity.TAG;
import android.content.Context;
-import android.content.SharedPreferences;
import android.util.Log;
import androidx.annotation.Keep;
@@ -34,22 +33,13 @@
import io.grpc.stub.StreamObserver;
-import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
final class DebianServiceImpl extends DebianServiceGrpc.DebianServiceImplBase {
- private static final String PREFERENCE_FILE_KEY =
- "com.android.virtualization.terminal.PREFERENCE_FILE_KEY";
- private static final String PREFERENCE_FORWARDING_PORTS = "PREFERENCE_FORWARDING_PORTS";
- private static final String PREFERENCE_FORWARDING_PORT_IS_ENABLED_PREFIX =
- "PREFERENCE_FORWARDING_PORT_IS_ENABLED_";
-
private final Context mContext;
- private final SharedPreferences mSharedPref;
- private final String mPreferenceForwardingPorts;
- private final String mPreferenceForwardingPortIsEnabled;
- private SharedPreferences.OnSharedPreferenceChangeListener mPortForwardingListener;
+ private final PortsStateManager mPortsStateManager;
+ private PortsStateManager.Listener mPortsStateListener;
private final DebianServiceCallback mCallback;
static {
@@ -60,12 +50,7 @@
super();
mCallback = callback;
mContext = context;
- mSharedPref =
- mContext.getSharedPreferences(
- mContext.getString(R.string.preference_file_key), Context.MODE_PRIVATE);
- mPreferenceForwardingPorts = mContext.getString(R.string.preference_forwarding_ports);
- mPreferenceForwardingPortIsEnabled =
- mContext.getString(R.string.preference_forwarding_port_is_enabled);
+ mPortsStateManager = PortsStateManager.getInstance(mContext);
}
@Override
@@ -73,23 +58,7 @@
ReportVmActivePortsRequest request,
StreamObserver<ReportVmActivePortsResponse> responseObserver) {
Log.d(TAG, "reportVmActivePorts: " + request.toString());
-
- Set<String> prevPorts =
- mSharedPref.getStringSet(mPreferenceForwardingPorts, Collections.emptySet());
- SharedPreferences.Editor editor = mSharedPref.edit();
- Set<String> ports = new HashSet<>();
- for (int port : request.getPortsList()) {
- ports.add(Integer.toString(port));
- if (!mSharedPref.contains(
- mPreferenceForwardingPortIsEnabled + Integer.toString(port))) {
- editor.putBoolean(
- mPreferenceForwardingPortIsEnabled + Integer.toString(port), false);
- }
- }
- editor.putStringSet(mPreferenceForwardingPorts, ports);
- editor.apply();
- mCallback.onActivePortsChanged(prevPorts, ports);
-
+ mPortsStateManager.updateActivePorts(new HashSet<>(request.getPortsList()));
ReportVmActivePortsResponse reply =
ReportVmActivePortsResponse.newBuilder().setSuccess(true).build();
responseObserver.onNext(reply);
@@ -110,18 +79,15 @@
public void openForwardingRequestQueue(
QueueOpeningRequest request, StreamObserver<ForwardingRequestItem> responseObserver) {
Log.d(TAG, "OpenForwardingRequestQueue");
- mPortForwardingListener =
- new SharedPreferences.OnSharedPreferenceChangeListener() {
+ mPortsStateListener =
+ new PortsStateManager.Listener() {
@Override
- public void onSharedPreferenceChanged(
- SharedPreferences sharedPreferences, String key) {
- if (key.startsWith(mPreferenceForwardingPortIsEnabled)
- || key.equals(mPreferenceForwardingPorts)) {
- updateListeningPorts();
- }
+ public void onPortsStateUpdated(
+ Set<Integer> oldActivePorts, Set<Integer> newActivePorts) {
+ updateListeningPorts();
}
};
- mSharedPref.registerOnSharedPreferenceChangeListener(mPortForwardingListener);
+ mPortsStateManager.registerListener(mPortsStateListener);
updateListeningPorts();
runForwarderHost(request.getCid(), new ForwarderHostCallback(responseObserver));
responseObserver.onCompleted();
@@ -151,31 +117,26 @@
void killForwarderHost() {
Log.d(TAG, "Stopping port forwarding");
- if (mPortForwardingListener != null) {
- mSharedPref.unregisterOnSharedPreferenceChangeListener(mPortForwardingListener);
- terminateForwarderHost();
+ if (mPortsStateListener != null) {
+ mPortsStateManager.unregisterListener(mPortsStateListener);
+ mPortsStateListener = null;
}
+ terminateForwarderHost();
}
private static native void updateListeningPorts(int[] ports);
private void updateListeningPorts() {
+ Set<Integer> activePorts = mPortsStateManager.getActivePorts();
+ Set<Integer> enabledPorts = mPortsStateManager.getEnabledPorts();
updateListeningPorts(
- mSharedPref
- .getStringSet(mPreferenceForwardingPorts, Collections.emptySet())
- .stream()
- .filter(
- port ->
- mSharedPref.getBoolean(
- mPreferenceForwardingPortIsEnabled + port, false))
- .map(Integer::valueOf)
+ activePorts.stream()
+ .filter(port -> enabledPorts.contains(port))
.mapToInt(Integer::intValue)
.toArray());
}
protected interface DebianServiceCallback {
void onIpAddressAvailable(String ipAddr);
-
- void onActivePortsChanged(Set<String> oldPorts, Set<String> newPorts);
}
}
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/PortNotifier.java b/android/TerminalApp/java/com/android/virtualization/terminal/PortNotifier.java
index 2f728ba..0d70ab9 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/PortNotifier.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/PortNotifier.java
@@ -27,7 +27,6 @@
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.drawable.Icon;
-import android.util.Log;
import java.util.HashSet;
import java.util.Locale;
@@ -45,35 +44,39 @@
private final Context mContext;
private final NotificationManager mNotificationManager;
private final BroadcastReceiver mReceiver;
+ private final PortsStateManager mPortsStateManager;
+ private final PortsStateManager.Listener mPortsStateListener;
public PortNotifier(Context context) {
mContext = context;
mNotificationManager = mContext.getSystemService(NotificationManager.class);
mReceiver = new PortForwardingRequestReceiver();
+ mPortsStateManager = PortsStateManager.getInstance(mContext);
+ mPortsStateListener =
+ new PortsStateManager.Listener() {
+ @Override
+ public void onPortsStateUpdated(
+ Set<Integer> oldActivePorts, Set<Integer> newActivePorts) {
+ Set<Integer> union = new HashSet<>(oldActivePorts);
+ union.addAll(newActivePorts);
+ for (int port : union) {
+ if (!oldActivePorts.contains(port)) {
+ showNotificationFor(port);
+ } else if (!newActivePorts.contains(port)) {
+ discardNotificationFor(port);
+ }
+ }
+ }
+ };
+ mPortsStateManager.registerListener(mPortsStateListener);
+
IntentFilter intentFilter = new IntentFilter(ACTION_PORT_FORWARDING);
mContext.registerReceiver(mReceiver, intentFilter, Context.RECEIVER_NOT_EXPORTED);
}
- public void onActivePortsChanged(Set<String> oldPorts, Set<String> newPorts) {
- Set<String> union = new HashSet<>(oldPorts);
- union.addAll(newPorts);
- for (String portStr : union) {
- try {
- int port = Integer.parseInt(portStr);
- if (!oldPorts.contains(portStr)) {
- showNotificationFor(port);
- } else if (!newPorts.contains(portStr)) {
- discardNotificationFor(port);
- }
- } catch (NumberFormatException e) {
- Log.e(TAG, "Failed to parse port: " + portStr);
- throw e;
- }
- }
- }
-
public void stop() {
+ mPortsStateManager.unregisterListener(mPortsStateListener);
mContext.unregisterReceiver(mReceiver);
}
@@ -134,16 +137,9 @@
}
private void performActionPortForwarding(Context context, Intent intent) {
- String prefKey = context.getString(R.string.preference_file_key);
int port = intent.getIntExtra(KEY_PORT, 0);
- String key = context.getString(R.string.preference_forwarding_port_is_enabled) + port;
boolean enabled = intent.getBooleanExtra(KEY_ENABLED, false);
-
- context.getSharedPreferences(prefKey, Context.MODE_PRIVATE)
- .edit()
- .putBoolean(key, enabled)
- .apply();
-
+ mPortsStateManager.updateEnabledPort(port, enabled);
discardNotificationFor(port);
}
}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/PortsStateManager.java b/android/TerminalApp/java/com/android/virtualization/terminal/PortsStateManager.java
new file mode 100644
index 0000000..56ecd96
--- /dev/null
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/PortsStateManager.java
@@ -0,0 +1,146 @@
+/*
+ * 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.
+ */
+
+package com.android.virtualization.terminal;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * PortsStateManager is responsible for communicating with shared preferences and managing state of
+ * ports.
+ */
+public class PortsStateManager {
+ private static final String PREFS_NAME = ".PORTS";
+ private static final int FLAG_ENABLED = 1;
+
+ private static PortsStateManager mInstance;
+ private final Object mLock = new Object();
+
+ private final SharedPreferences mSharedPref;
+
+ @GuardedBy("mLock")
+ private Set<Integer> mActivePorts;
+
+ @GuardedBy("mLock")
+ private final Set<Integer> mEnabledPorts;
+
+ @GuardedBy("mLock")
+ private final Set<Listener> mListeners;
+
+ private PortsStateManager(SharedPreferences sharedPref) {
+ mSharedPref = sharedPref;
+ mEnabledPorts =
+ mSharedPref.getAll().entrySet().stream()
+ .filter(entry -> entry.getValue() instanceof Integer)
+ .filter(entry -> ((int) entry.getValue() & FLAG_ENABLED) == FLAG_ENABLED)
+ .map(entry -> entry.getKey())
+ .filter(
+ key -> {
+ try {
+ Integer.parseInt(key);
+ return true;
+ } catch (NumberFormatException e) {
+ return false;
+ }
+ })
+ .map(Integer::parseInt)
+ .collect(Collectors.toSet());
+ mActivePorts = new HashSet<>();
+ mListeners = new HashSet<>();
+ }
+
+ static synchronized PortsStateManager getInstance(Context context) {
+ if (mInstance == null) {
+ SharedPreferences sharedPref =
+ context.getSharedPreferences(
+ context.getPackageName() + PREFS_NAME, Context.MODE_PRIVATE);
+ mInstance = new PortsStateManager(sharedPref);
+ }
+ return mInstance;
+ }
+
+ Set<Integer> getActivePorts() {
+ synchronized (mLock) {
+ return new HashSet<>(mActivePorts);
+ }
+ }
+
+ Set<Integer> getEnabledPorts() {
+ synchronized (mLock) {
+ return new HashSet<>(mEnabledPorts);
+ }
+ }
+
+ void updateActivePorts(Set<Integer> ports) {
+ Set<Integer> oldPorts;
+ synchronized (mLock) {
+ oldPorts = mActivePorts;
+ mActivePorts = ports;
+ }
+ notifyPortsStateUpdated(oldPorts, ports);
+ }
+
+ void updateEnabledPort(int port, boolean enabled) {
+ Set<Integer> activePorts;
+ synchronized (mLock) {
+ SharedPreferences.Editor editor = mSharedPref.edit();
+ editor.putInt(String.valueOf(port), enabled ? FLAG_ENABLED : 0);
+ editor.apply();
+ if (enabled) {
+ mEnabledPorts.add(port);
+ } else {
+ mEnabledPorts.remove(port);
+ }
+ activePorts = mActivePorts;
+ }
+ notifyPortsStateUpdated(activePorts, activePorts);
+ }
+
+ void registerListener(Listener listener) {
+ synchronized (mLock) {
+ mListeners.add(listener);
+ }
+ }
+
+ void unregisterListener(Listener listener) {
+ synchronized (mLock) {
+ mListeners.remove(listener);
+ }
+ }
+
+ private void notifyPortsStateUpdated(Set<Integer> oldActivePorts, Set<Integer> newActivePorts) {
+ Set<Listener> listeners;
+ synchronized (mLock) {
+ listeners = new HashSet<>(mListeners);
+ }
+ for (Listener listener : listeners) {
+ listener.onPortsStateUpdated(
+ new HashSet<>(oldActivePorts), new HashSet<>(newActivePorts));
+ }
+ }
+
+ interface Listener {
+ default void onPortsStateUpdated(
+ Set<Integer> oldActivePorts, Set<Integer> newActivePorts) {}
+ }
+}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsPortForwardingActivity.kt b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsPortForwardingActivity.kt
index fe693c4..d64c267 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsPortForwardingActivity.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsPortForwardingActivity.kt
@@ -15,25 +15,21 @@
*/
package com.android.virtualization.terminal
-import android.content.Context
-import android.content.SharedPreferences
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
class SettingsPortForwardingActivity : AppCompatActivity() {
- private lateinit var mSharedPref: SharedPreferences
+ private lateinit var mPortsStateManager: PortsStateManager
private lateinit var mAdapter: SettingsPortForwardingAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.settings_port_forwarding)
- mSharedPref =
- this.getSharedPreferences(getString(R.string.preference_file_key), Context.MODE_PRIVATE)
-
- mAdapter = SettingsPortForwardingAdapter(mSharedPref, this)
+ mPortsStateManager = PortsStateManager.getInstance(this)
+ mAdapter = SettingsPortForwardingAdapter(mPortsStateManager)
val recyclerView: RecyclerView = findViewById(R.id.settings_port_forwarding_recycler_view)
recyclerView.layoutManager = LinearLayoutManager(this)
@@ -42,11 +38,11 @@
override fun onResume() {
super.onResume()
- mSharedPref.registerOnSharedPreferenceChangeListener(mAdapter)
+ mAdapter.registerPortsStateListener()
}
override fun onPause() {
- mSharedPref.unregisterOnSharedPreferenceChangeListener(mAdapter)
+ mAdapter.unregisterPortsStateListener()
super.onPause()
}
}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsPortForwardingAdapter.kt b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsPortForwardingAdapter.kt
index afe985a..8282910 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsPortForwardingAdapter.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsPortForwardingAdapter.kt
@@ -15,8 +15,6 @@
*/
package com.android.virtualization.terminal
-import android.content.Context
-import android.content.SharedPreferences
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@@ -26,14 +24,11 @@
import androidx.recyclerview.widget.SortedListAdapterCallback
import com.google.android.material.materialswitch.MaterialSwitch
-class SettingsPortForwardingAdapter(
- private val sharedPref: SharedPreferences?,
- private val context: Context,
-) :
- RecyclerView.Adapter<SettingsPortForwardingAdapter.ViewHolder>(),
- SharedPreferences.OnSharedPreferenceChangeListener {
+class SettingsPortForwardingAdapter(private val mPortsStateManager: PortsStateManager) :
+ RecyclerView.Adapter<SettingsPortForwardingAdapter.ViewHolder>() {
private var mItems: SortedList<SettingsPortForwardingItem>
+ private val mPortsStateListener: Listener
init {
mItems =
@@ -63,24 +58,24 @@
},
)
mItems.addAll(getCurrentSettingsPortForwardingItem())
+ mPortsStateListener = Listener()
+ }
+
+ fun registerPortsStateListener() {
+ mPortsStateManager.registerListener(mPortsStateListener)
+ mItems.replaceAll(getCurrentSettingsPortForwardingItem())
+ }
+
+ fun unregisterPortsStateListener() {
+ mPortsStateManager.unregisterListener(mPortsStateListener)
}
private fun getCurrentSettingsPortForwardingItem(): ArrayList<SettingsPortForwardingItem> {
- val items = ArrayList<SettingsPortForwardingItem>()
- val ports =
- sharedPref!!.getStringSet(
- context.getString(R.string.preference_forwarding_ports),
- HashSet<String>(),
- )
- for (port in ports!!) {
- val enabled =
- sharedPref.getBoolean(
- context.getString(R.string.preference_forwarding_port_is_enabled) + port,
- false,
- )
- items.add(SettingsPortForwardingItem(port.toInt(), enabled))
- }
- return items
+ val enabledPorts = mPortsStateManager.getEnabledPorts()
+ return mPortsStateManager
+ .getActivePorts()
+ .map { SettingsPortForwardingItem(it, enabledPorts.contains(it)) }
+ .toCollection(ArrayList())
}
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
@@ -97,32 +92,19 @@
}
override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) {
- viewHolder.port.text = mItems[position].port.toString()
+ val port = mItems[position].port
+ viewHolder.port.text = port.toString()
viewHolder.enabledSwitch.contentDescription = viewHolder.port.text
viewHolder.enabledSwitch.isChecked = mItems[position].enabled
viewHolder.enabledSwitch.setOnCheckedChangeListener { _, isChecked ->
- val sharedPref: SharedPreferences =
- context.getSharedPreferences(
- context.getString(R.string.preference_file_key),
- Context.MODE_PRIVATE,
- )
- val editor = sharedPref.edit()
- editor.putBoolean(
- context.getString(R.string.preference_forwarding_port_is_enabled) +
- viewHolder.port.text,
- isChecked,
- )
- editor.apply()
+ mPortsStateManager.updateEnabledPort(port, isChecked)
}
}
override fun getItemCount() = mItems.size()
- override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
- if (
- key == context.getString(R.string.preference_forwarding_ports) ||
- key!!.startsWith(context.getString(R.string.preference_forwarding_port_is_enabled))
- ) {
+ private inner class Listener : PortsStateManager.Listener {
+ override fun onPortsStateUpdated(oldActivePorts: Set<Int>, newActivePorts: Set<Int>) {
mItems.replaceAll(getCurrentSettingsPortForwardingItem())
}
}
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
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.java b/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.java
index c2d224a..a82c688 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.java
@@ -291,11 +291,6 @@
mResultReceiver.send(VmLauncherService.RESULT_IPADDR, b);
}
- @Override
- public void onActivePortsChanged(Set<String> oldPorts, Set<String> newPorts) {
- mPortNotifier.onActivePortsChanged(oldPorts, newPorts);
- }
-
public static void stop(Context context) {
Intent i = getMyIntent(context);
context.stopService(i);
diff --git a/android/TerminalApp/res/values/config.xml b/android/TerminalApp/res/values/config.xml
index 6440ee6..713e1a5 100644
--- a/android/TerminalApp/res/values/config.xml
+++ b/android/TerminalApp/res/values/config.xml
@@ -17,8 +17,6 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="preference_file_key" translatable="false">com.android.virtualization.terminal.PREFERENCE_FILE_KEY</string>
<string name="preference_disk_size_key" translatable="false">PREFERENCE_DISK_SIZE_KEY</string>
- <string name="preference_forwarding_ports" translatable="false">PREFERENCE_FORWARDING_PORTS</string>
- <string name="preference_forwarding_port_is_enabled" translatable="false">PREFERENCE_FORWARDING_PORT_IS_ENABLED_</string>
<bool name="terminal_portrait_only">true</bool>
</resources>
diff --git a/android/TerminalApp/res/values/strings.xml b/android/TerminalApp/res/values/strings.xml
index 44d88a2..884e5f0 100644
--- a/android/TerminalApp/res/values/strings.xml
+++ b/android/TerminalApp/res/values/strings.xml
@@ -81,17 +81,31 @@
<!-- Dialog confirmation button for restarting the terminal [CHAR LIMIT=16] -->
<string name="settings_disk_resize_resize_confirm_dialog_confirm">Confirm</string>
- <!-- Settings menu title for 'port forwarding' [CHAR LIMIT=none] -->
- <string name="settings_port_forwarding_title">Port forwarding</string>
- <!-- Settings menu subtitle for 'port forwarding' [CHAR LIMIT=none] -->
- <string name="settings_port_forwarding_sub_title">Configure port forwarding</string>
- <!-- Notification title for new port forwarding [CHAR LIMIT=none] -->
+ <!-- Settings menu title for port forwarding [CHAR LIMIT=none] -->
+ <string name="settings_port_forwarding_title">Port control</string>
+ <!-- Settings menu subtitle for port forwarding [CHAR LIMIT=none] -->
+ <string name="settings_port_forwarding_sub_title">Allow/deny listening ports</string>
+ <!-- Title for active ports setting in port forwarding [CHAR LIMIT=none] -->
+ <string name="settings_port_forwarding_active_ports_title">Listening ports</string>
+ <!-- Title for other enabled ports setting in port forwarding [CHAR LIMIT=none] -->
+ <string name="settings_port_forwarding_other_enabled_ports_title">Saved allowed ports</string>
+
+ <!-- Dialog title for enabling a new port [CHAR LIMIT=none] -->
+ <string name="settings_port_forwarding_dialog_title">Allow a new port</string>
+ <!-- Dialog hint for enabling a new port [CHAR LIMIT=none] -->
+ <string name="settings_port_forwarding_dialog_textview_hint">Enter a new port number</string>
+ <!-- Dialog save action for enabling a new port [CHAR LIMIT=16] -->
+ <string name="settings_port_forwarding_dialog_save">Save</string>
+ <!-- Dialog cancel action for enabling a new port [CHAR LIMIT=16] -->
+ <string name="settings_port_forwarding_dialog_cancel">Cancel</string>
+
+ <!-- Notification title for a new active port [CHAR LIMIT=none] -->
<string name="settings_port_forwarding_notification_title">Terminal is requesting to open a new port</string>
- <!-- Notification content for new port forwarding [CHAR LIMIT=none] -->
+ <!-- Notification content for a new active port [CHAR LIMIT=none] -->
<string name="settings_port_forwarding_notification_content">Port requested: <xliff:g id="port_number" example="8080">%d</xliff:g></string>
- <!-- Notification action accept [CHAR LIMIT=none] -->
+ <!-- Notification action accept [CHAR LIMIT=16] -->
<string name="settings_port_forwarding_notification_accept">Accept</string>
- <!-- Notification action deny [CHAR LIMIT=none] -->
+ <!-- Notification action deny [CHAR LIMIT=16] -->
<string name="settings_port_forwarding_notification_deny">Deny</string>
<!-- Settings menu title for recoverying image [CHAR LIMIT=none] -->