aboutsummaryrefslogtreecommitdiffstats
path: root/app/src/main/java/org/connectbot/service/TerminalKeyListener.java
diff options
context:
space:
mode:
authorKenny Root <kenny@the-b.org>2014-10-01 23:04:51 +0100
committerKenny Root <kenny@the-b.org>2014-10-01 12:48:19 +0100
commit49b779dcaf03e3598d2709b321e20ea029b25163 (patch)
tree05af547b1f1433d7dd6f7373d0b25a455e053a03 /app/src/main/java/org/connectbot/service/TerminalKeyListener.java
parentd64786d9197090c74072b648e487e3d34817bb57 (diff)
downloadconnectbot-49b779dcaf03e3598d2709b321e20ea029b25163.tar.gz
connectbot-49b779dcaf03e3598d2709b321e20ea029b25163.tar.bz2
connectbot-49b779dcaf03e3598d2709b321e20ea029b25163.zip
Convert to gradle build system
Diffstat (limited to 'app/src/main/java/org/connectbot/service/TerminalKeyListener.java')
-rw-r--r--app/src/main/java/org/connectbot/service/TerminalKeyListener.java558
1 files changed, 558 insertions, 0 deletions
diff --git a/app/src/main/java/org/connectbot/service/TerminalKeyListener.java b/app/src/main/java/org/connectbot/service/TerminalKeyListener.java
new file mode 100644
index 0000000..7ff21df
--- /dev/null
+++ b/app/src/main/java/org/connectbot/service/TerminalKeyListener.java
@@ -0,0 +1,558 @@
+/*
+ * ConnectBot: simple, powerful, open-source SSH client for Android
+ * Copyright 2010 Kenny Root, Jeffrey Sharkey
+ *
+ * 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 org.connectbot.service;
+
+import java.io.IOException;
+
+import org.connectbot.TerminalView;
+import org.connectbot.bean.SelectionArea;
+import org.connectbot.util.PreferenceConstants;
+
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
+import android.content.res.Configuration;
+import android.preference.PreferenceManager;
+import android.text.ClipboardManager;
+import android.util.Log;
+import android.view.KeyCharacterMap;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.View.OnKeyListener;
+import de.mud.terminal.VDUBuffer;
+import de.mud.terminal.vt320;
+
+/**
+ * @author kenny
+ *
+ */
+@SuppressWarnings("deprecation") // for ClipboardManager
+public class TerminalKeyListener implements OnKeyListener, OnSharedPreferenceChangeListener {
+ private static final String TAG = "ConnectBot.OnKeyListener";
+
+ // Constants for our private tracking of modifier state
+ public final static int OUR_CTRL_ON = 0x01;
+ public final static int OUR_CTRL_LOCK = 0x02;
+ public final static int OUR_ALT_ON = 0x04;
+ public final static int OUR_ALT_LOCK = 0x08;
+ public final static int OUR_SHIFT_ON = 0x10;
+ public final static int OUR_SHIFT_LOCK = 0x20;
+ private final static int OUR_SLASH = 0x40;
+ private final static int OUR_TAB = 0x80;
+
+ // All the transient key codes
+ private final static int OUR_TRANSIENT = OUR_CTRL_ON | OUR_ALT_ON
+ | OUR_SHIFT_ON | OUR_SLASH | OUR_TAB;
+
+ // The bit mask of momentary and lock states for each
+ private final static int OUR_CTRL_MASK = OUR_CTRL_ON | OUR_CTRL_LOCK;
+ private final static int OUR_ALT_MASK = OUR_ALT_ON | OUR_ALT_LOCK;
+ private final static int OUR_SHIFT_MASK = OUR_SHIFT_ON | OUR_SHIFT_LOCK;
+
+ // backport constants from api level 11
+ private final static int KEYCODE_ESCAPE = 111;
+ private final static int HC_META_CTRL_ON = 0x1000;
+ private final static int HC_META_CTRL_LEFT_ON = 0x2000;
+ private final static int HC_META_CTRL_RIGHT_ON = 0x4000;
+ private final static int HC_META_CTRL_MASK = HC_META_CTRL_ON | HC_META_CTRL_RIGHT_ON
+ | HC_META_CTRL_LEFT_ON;
+ private final static int HC_META_ALT_MASK = KeyEvent.META_ALT_ON | KeyEvent.META_ALT_LEFT_ON
+ | KeyEvent.META_ALT_RIGHT_ON;
+
+ private final TerminalManager manager;
+ private final TerminalBridge bridge;
+ private final VDUBuffer buffer;
+
+ private String keymode = null;
+ private final boolean deviceHasHardKeyboard;
+ private boolean shiftedNumbersAreFKeysOnHardKeyboard;
+ private boolean controlNumbersAreFKeysOnSoftKeyboard;
+ private boolean volumeKeysChangeFontSize;
+
+ private int ourMetaState = 0;
+
+ private int mDeadKey = 0;
+
+ // TODO add support for the new API.
+ private ClipboardManager clipboard = null;
+
+ private boolean selectingForCopy = false;
+ private final SelectionArea selectionArea;
+
+ private String encoding;
+
+ private final SharedPreferences prefs;
+
+ public TerminalKeyListener(TerminalManager manager,
+ TerminalBridge bridge,
+ VDUBuffer buffer,
+ String encoding) {
+ this.manager = manager;
+ this.bridge = bridge;
+ this.buffer = buffer;
+ this.encoding = encoding;
+
+ selectionArea = new SelectionArea();
+
+ prefs = PreferenceManager.getDefaultSharedPreferences(manager);
+ prefs.registerOnSharedPreferenceChangeListener(this);
+
+ deviceHasHardKeyboard = (manager.res.getConfiguration().keyboard
+ == Configuration.KEYBOARD_QWERTY);
+
+ updatePrefs();
+ }
+
+ /**
+ * Handle onKey() events coming down from a {@link TerminalView} above us.
+ * Modify the keys to make more sense to a host then pass it to the transport.
+ */
+ public boolean onKey(View v, int keyCode, KeyEvent event) {
+ try {
+ // skip keys if we aren't connected yet or have been disconnected
+ if (bridge.isDisconnected() || bridge.transport == null)
+ return false;
+
+ final boolean interpretAsHardKeyboard = deviceHasHardKeyboard &&
+ !manager.hardKeyboardHidden;
+ final boolean rightModifiersAreSlashAndTab = interpretAsHardKeyboard &&
+ PreferenceConstants.KEYMODE_RIGHT.equals(keymode);
+ final boolean leftModifiersAreSlashAndTab = interpretAsHardKeyboard &&
+ PreferenceConstants.KEYMODE_LEFT.equals(keymode);
+ final boolean shiftedNumbersAreFKeys = shiftedNumbersAreFKeysOnHardKeyboard &&
+ interpretAsHardKeyboard;
+ final boolean controlNumbersAreFKeys = controlNumbersAreFKeysOnSoftKeyboard &&
+ !interpretAsHardKeyboard;
+
+ // Ignore all key-up events except for the special keys
+ if (event.getAction() == KeyEvent.ACTION_UP) {
+ if (rightModifiersAreSlashAndTab) {
+ if (keyCode == KeyEvent.KEYCODE_ALT_RIGHT
+ && (ourMetaState & OUR_SLASH) != 0) {
+ ourMetaState &= ~OUR_TRANSIENT;
+ bridge.transport.write('/');
+ return true;
+ } else if (keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT
+ && (ourMetaState & OUR_TAB) != 0) {
+ ourMetaState &= ~OUR_TRANSIENT;
+ bridge.transport.write(0x09);
+ return true;
+ }
+ } else if (leftModifiersAreSlashAndTab) {
+ if (keyCode == KeyEvent.KEYCODE_ALT_LEFT
+ && (ourMetaState & OUR_SLASH) != 0) {
+ ourMetaState &= ~OUR_TRANSIENT;
+ bridge.transport.write('/');
+ return true;
+ } else if (keyCode == KeyEvent.KEYCODE_SHIFT_LEFT
+ && (ourMetaState & OUR_TAB) != 0) {
+ ourMetaState &= ~OUR_TRANSIENT;
+ bridge.transport.write(0x09);
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ //Log.i("CBKeyDebug", KeyEventUtil.describeKeyEvent(keyCode, event));
+
+ if (volumeKeysChangeFontSize) {
+ if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
+ bridge.increaseFontSize();
+ return true;
+ } else if(keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
+ bridge.decreaseFontSize();
+ return true;
+ }
+ }
+
+ bridge.resetScrollPosition();
+
+ // Handle potentially multi-character IME input.
+ if (keyCode == KeyEvent.KEYCODE_UNKNOWN &&
+ event.getAction() == KeyEvent.ACTION_MULTIPLE) {
+ byte[] input = event.getCharacters().getBytes(encoding);
+ bridge.transport.write(input);
+ return true;
+ }
+
+ /// Handle alt and shift keys if they aren't repeating
+ if (event.getRepeatCount() == 0) {
+ if (rightModifiersAreSlashAndTab) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_ALT_RIGHT:
+ ourMetaState |= OUR_SLASH;
+ return true;
+ case KeyEvent.KEYCODE_SHIFT_RIGHT:
+ ourMetaState |= OUR_TAB;
+ return true;
+ case KeyEvent.KEYCODE_SHIFT_LEFT:
+ metaPress(OUR_SHIFT_ON);
+ return true;
+ case KeyEvent.KEYCODE_ALT_LEFT:
+ metaPress(OUR_ALT_ON);
+ return true;
+ }
+ } else if (leftModifiersAreSlashAndTab) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_ALT_LEFT:
+ ourMetaState |= OUR_SLASH;
+ return true;
+ case KeyEvent.KEYCODE_SHIFT_LEFT:
+ ourMetaState |= OUR_TAB;
+ return true;
+ case KeyEvent.KEYCODE_SHIFT_RIGHT:
+ metaPress(OUR_SHIFT_ON);
+ return true;
+ case KeyEvent.KEYCODE_ALT_RIGHT:
+ metaPress(OUR_ALT_ON);
+ return true;
+ }
+ } else {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_ALT_LEFT:
+ case KeyEvent.KEYCODE_ALT_RIGHT:
+ metaPress(OUR_ALT_ON);
+ return true;
+ case KeyEvent.KEYCODE_SHIFT_LEFT:
+ case KeyEvent.KEYCODE_SHIFT_RIGHT:
+ metaPress(OUR_SHIFT_ON);
+ return true;
+ }
+ }
+ }
+
+ if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
+ if (selectingForCopy) {
+ if (selectionArea.isSelectingOrigin())
+ selectionArea.finishSelectingOrigin();
+ else {
+ if (clipboard != null) {
+ // copy selected area to clipboard
+ String copiedText = selectionArea.copyFrom(buffer);
+ clipboard.setText(copiedText);
+ // XXX STOPSHIP
+// manager.notifyUser(manager.getString(
+// R.string.console_copy_done,
+// copiedText.length()));
+ selectingForCopy = false;
+ selectionArea.reset();
+ }
+ }
+ } else {
+ if ((ourMetaState & OUR_CTRL_ON) != 0) {
+ sendEscape();
+ ourMetaState &= ~OUR_CTRL_ON;
+ } else
+ metaPress(OUR_CTRL_ON);
+ }
+ bridge.redraw();
+ return true;
+ }
+
+ int derivedMetaState = event.getMetaState();
+ if ((ourMetaState & OUR_SHIFT_MASK) != 0)
+ derivedMetaState |= KeyEvent.META_SHIFT_ON;
+ if ((ourMetaState & OUR_ALT_MASK) != 0)
+ derivedMetaState |= KeyEvent.META_ALT_ON;
+ if ((ourMetaState & OUR_CTRL_MASK) != 0)
+ derivedMetaState |= HC_META_CTRL_ON;
+
+ if ((ourMetaState & OUR_TRANSIENT) != 0) {
+ ourMetaState &= ~OUR_TRANSIENT;
+ bridge.redraw();
+ }
+
+ // Test for modified numbers becoming function keys
+ if (shiftedNumbersAreFKeys && (derivedMetaState & KeyEvent.META_SHIFT_ON) != 0) {
+ if (sendFunctionKey(keyCode))
+ return true;
+ }
+ if (controlNumbersAreFKeys && (derivedMetaState & HC_META_CTRL_ON) != 0) {
+ if (sendFunctionKey(keyCode))
+ return true;
+ }
+
+ // Ask the system to use the keymap to give us the unicode character for this key,
+ // with our derived modifier state applied.
+ int uchar = event.getUnicodeChar(derivedMetaState & ~HC_META_CTRL_MASK);
+ int ucharWithoutAlt = event.getUnicodeChar(
+ derivedMetaState & ~(HC_META_ALT_MASK | HC_META_CTRL_MASK));
+ if (uchar != ucharWithoutAlt) {
+ // The alt key was used to modify the character returned; therefore, drop the alt
+ // modifier from the state so we don't end up sending alt+key.
+ derivedMetaState &= ~HC_META_ALT_MASK;
+ }
+
+ // Remove shift from the modifier state as it has already been used by getUnicodeChar.
+ derivedMetaState &= ~KeyEvent.META_SHIFT_ON;
+
+ if ((uchar & KeyCharacterMap.COMBINING_ACCENT) != 0) {
+ mDeadKey = uchar & KeyCharacterMap.COMBINING_ACCENT_MASK;
+ return true;
+ }
+
+ if (mDeadKey != 0) {
+ uchar = KeyCharacterMap.getDeadChar(mDeadKey, keyCode);
+ mDeadKey = 0;
+ }
+
+ // If we have a defined non-control character
+ if (uchar >= 0x20) {
+ if ((derivedMetaState & HC_META_CTRL_ON) != 0)
+ uchar = keyAsControl(uchar);
+ if ((derivedMetaState & KeyEvent.META_ALT_ON) != 0)
+ sendEscape();
+ if (uchar < 0x80)
+ bridge.transport.write(uchar);
+ else
+ // TODO write encoding routine that doesn't allocate each time
+ bridge.transport.write(new String(Character.toChars(uchar))
+ .getBytes(encoding));
+ return true;
+ }
+
+ // look for special chars
+ switch(keyCode) {
+ case KEYCODE_ESCAPE:
+ sendEscape();
+ return true;
+ case KeyEvent.KEYCODE_TAB:
+ bridge.transport.write(0x09);
+ return true;
+ case KeyEvent.KEYCODE_CAMERA:
+
+ // check to see which shortcut the camera button triggers
+ String camera = manager.prefs.getString(
+ PreferenceConstants.CAMERA,
+ PreferenceConstants.CAMERA_CTRLA_SPACE);
+ if(PreferenceConstants.CAMERA_CTRLA_SPACE.equals(camera)) {
+ bridge.transport.write(0x01);
+ bridge.transport.write(' ');
+ } else if(PreferenceConstants.CAMERA_CTRLA.equals(camera)) {
+ bridge.transport.write(0x01);
+ } else if(PreferenceConstants.CAMERA_ESC.equals(camera)) {
+ ((vt320)buffer).keyTyped(vt320.KEY_ESCAPE, ' ', 0);
+ } else if(PreferenceConstants.CAMERA_ESC_A.equals(camera)) {
+ ((vt320)buffer).keyTyped(vt320.KEY_ESCAPE, ' ', 0);
+ bridge.transport.write('a');
+ }
+
+ break;
+
+ case KeyEvent.KEYCODE_DEL:
+ ((vt320) buffer).keyPressed(vt320.KEY_BACK_SPACE, ' ',
+ getStateForBuffer());
+ return true;
+ case KeyEvent.KEYCODE_ENTER:
+ ((vt320)buffer).keyTyped(vt320.KEY_ENTER, ' ', 0);
+ return true;
+
+ case KeyEvent.KEYCODE_DPAD_LEFT:
+ if (selectingForCopy) {
+ selectionArea.decrementColumn();
+ bridge.redraw();
+ } else {
+ ((vt320) buffer).keyPressed(vt320.KEY_LEFT, ' ',
+ getStateForBuffer());
+ bridge.tryKeyVibrate();
+ }
+ return true;
+
+ case KeyEvent.KEYCODE_DPAD_UP:
+ if (selectingForCopy) {
+ selectionArea.decrementRow();
+ bridge.redraw();
+ } else {
+ ((vt320) buffer).keyPressed(vt320.KEY_UP, ' ',
+ getStateForBuffer());
+ bridge.tryKeyVibrate();
+ }
+ return true;
+
+ case KeyEvent.KEYCODE_DPAD_DOWN:
+ if (selectingForCopy) {
+ selectionArea.incrementRow();
+ bridge.redraw();
+ } else {
+ ((vt320) buffer).keyPressed(vt320.KEY_DOWN, ' ',
+ getStateForBuffer());
+ bridge.tryKeyVibrate();
+ }
+ return true;
+
+ case KeyEvent.KEYCODE_DPAD_RIGHT:
+ if (selectingForCopy) {
+ selectionArea.incrementColumn();
+ bridge.redraw();
+ } else {
+ ((vt320) buffer).keyPressed(vt320.KEY_RIGHT, ' ',
+ getStateForBuffer());
+ bridge.tryKeyVibrate();
+ }
+ return true;
+ }
+
+ } catch (IOException e) {
+ Log.e(TAG, "Problem while trying to handle an onKey() event", e);
+ try {
+ bridge.transport.flush();
+ } catch (IOException ioe) {
+ Log.d(TAG, "Our transport was closed, dispatching disconnect event");
+ bridge.dispatchDisconnect(false);
+ }
+ } catch (NullPointerException npe) {
+ Log.d(TAG, "Input before connection established ignored.");
+ return true;
+ }
+
+ return false;
+ }
+
+ public int keyAsControl(int key) {
+ // Support CTRL-a through CTRL-z
+ if (key >= 0x61 && key <= 0x7A)
+ key -= 0x60;
+ // Support CTRL-A through CTRL-_
+ else if (key >= 0x41 && key <= 0x5F)
+ key -= 0x40;
+ // CTRL-space sends NULL
+ else if (key == 0x20)
+ key = 0x00;
+ // CTRL-? sends DEL
+ else if (key == 0x3F)
+ key = 0x7F;
+ return key;
+ }
+
+ public void sendEscape() {
+ ((vt320)buffer).keyTyped(vt320.KEY_ESCAPE, ' ', 0);
+ }
+
+ /**
+ * @param key
+ * @return successful
+ */
+ private boolean sendFunctionKey(int keyCode) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_1:
+ ((vt320) buffer).keyPressed(vt320.KEY_F1, ' ', 0);
+ return true;
+ case KeyEvent.KEYCODE_2:
+ ((vt320) buffer).keyPressed(vt320.KEY_F2, ' ', 0);
+ return true;
+ case KeyEvent.KEYCODE_3:
+ ((vt320) buffer).keyPressed(vt320.KEY_F3, ' ', 0);
+ return true;
+ case KeyEvent.KEYCODE_4:
+ ((vt320) buffer).keyPressed(vt320.KEY_F4, ' ', 0);
+ return true;
+ case KeyEvent.KEYCODE_5:
+ ((vt320) buffer).keyPressed(vt320.KEY_F5, ' ', 0);
+ return true;
+ case KeyEvent.KEYCODE_6:
+ ((vt320) buffer).keyPressed(vt320.KEY_F6, ' ', 0);
+ return true;
+ case KeyEvent.KEYCODE_7:
+ ((vt320) buffer).keyPressed(vt320.KEY_F7, ' ', 0);
+ return true;
+ case KeyEvent.KEYCODE_8:
+ ((vt320) buffer).keyPressed(vt320.KEY_F8, ' ', 0);
+ return true;
+ case KeyEvent.KEYCODE_9:
+ ((vt320) buffer).keyPressed(vt320.KEY_F9, ' ', 0);
+ return true;
+ case KeyEvent.KEYCODE_0:
+ ((vt320) buffer).keyPressed(vt320.KEY_F10, ' ', 0);
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Handle meta key presses where the key can be locked on.
+ * <p>
+ * 1st press: next key to have meta state<br />
+ * 2nd press: meta state is locked on<br />
+ * 3rd press: disable meta state
+ *
+ * @param code
+ */
+ public void metaPress(int code) {
+ if ((ourMetaState & (code << 1)) != 0) {
+ ourMetaState &= ~(code << 1);
+ } else if ((ourMetaState & code) != 0) {
+ ourMetaState &= ~code;
+ ourMetaState |= code << 1;
+ } else
+ ourMetaState |= code;
+ bridge.redraw();
+ }
+
+ public void setTerminalKeyMode(String keymode) {
+ this.keymode = keymode;
+ }
+
+ private int getStateForBuffer() {
+ int bufferState = 0;
+
+ if ((ourMetaState & OUR_CTRL_MASK) != 0)
+ bufferState |= vt320.KEY_CONTROL;
+ if ((ourMetaState & OUR_SHIFT_MASK) != 0)
+ bufferState |= vt320.KEY_SHIFT;
+ if ((ourMetaState & OUR_ALT_MASK) != 0)
+ bufferState |= vt320.KEY_ALT;
+
+ return bufferState;
+ }
+
+ public int getMetaState() {
+ return ourMetaState;
+ }
+
+ public int getDeadKey() {
+ return mDeadKey;
+ }
+
+ public void setClipboardManager(ClipboardManager clipboard) {
+ this.clipboard = clipboard;
+ }
+
+ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
+ String key) {
+ if (PreferenceConstants.KEYMODE.equals(key) ||
+ PreferenceConstants.SHIFT_FKEYS.equals(key) ||
+ PreferenceConstants.CTRL_FKEYS.equals(key) ||
+ PreferenceConstants.VOLUME_FONT.equals(key)) {
+ updatePrefs();
+ }
+ }
+
+ private void updatePrefs() {
+ keymode = prefs.getString(PreferenceConstants.KEYMODE, PreferenceConstants.KEYMODE_RIGHT);
+ shiftedNumbersAreFKeysOnHardKeyboard =
+ prefs.getBoolean(PreferenceConstants.SHIFT_FKEYS, false);
+ controlNumbersAreFKeysOnSoftKeyboard =
+ prefs.getBoolean(PreferenceConstants.CTRL_FKEYS, false);
+ volumeKeysChangeFontSize = prefs.getBoolean(PreferenceConstants.VOLUME_FONT, true);
+ }
+
+ public void setCharset(String encoding) {
+ this.encoding = encoding;
+ }
+}