/* * 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.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 = "CB.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 KEYCODE_CTRL_LEFT = 113; private final static int KEYCODE_CTRL_RIGHT = 114; private final static int KEYCODE_INSERT = 124; private final static int KEYCODE_FORWARD_DEL = 112; private final static int KEYCODE_MOVE_HOME = 122; private final static int KEYCODE_MOVE_END = 123; private final static int KEYCODE_PAGE_DOWN = 93; private final static int KEYCODE_PAGE_UP = 92; 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 stickyMetas; 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 org.connectbot.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 == KEYCODE_CTRL_LEFT || keyCode == KEYCODE_CTRL_RIGHT) { metaPress(OUR_CTRL_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, true); } 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; } // CTRL-SHIFT-C to copy. if (keyCode == KeyEvent.KEYCODE_C && (derivedMetaState & HC_META_CTRL_ON) != 0 && (derivedMetaState & KeyEvent.META_SHIFT_ON) != 0) { bridge.copyCurrentSelection(); return true; } // CTRL-SHIFT-V to paste. if (keyCode == KeyEvent.KEYCODE_V && (derivedMetaState & HC_META_CTRL_ON) != 0 && (derivedMetaState & KeyEvent.META_SHIFT_ON) != 0 && clipboard.hasText()) { bridge.injectString(clipboard.getText().toString()); return true; } if ((keyCode == KeyEvent.KEYCODE_EQUALS && (derivedMetaState & HC_META_CTRL_ON) != 0 && (derivedMetaState & KeyEvent.META_SHIFT_ON) != 0) || (keyCode == KeyEvent.KEYCODE_PLUS && (derivedMetaState & HC_META_CTRL_ON) != 0)) { bridge.increaseFontSize(); return true; } if (keyCode == KeyEvent.KEYCODE_MINUS && (derivedMetaState & HC_META_CTRL_ON) != 0) { bridge.decreaseFontSize(); 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 == 0) { // Keymap doesn't know the key with alt on it, so just go with the unmodified version uchar = ucharWithoutAlt; } else 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; case KEYCODE_INSERT: ((vt320) buffer).keyPressed(vt320.KEY_INSERT, ' ', getStateForBuffer()); return true; case KEYCODE_FORWARD_DEL: ((vt320) buffer).keyPressed(vt320.KEY_DELETE, ' ', getStateForBuffer()); return true; case KEYCODE_MOVE_HOME: ((vt320) buffer).keyPressed(vt320.KEY_HOME, ' ', getStateForBuffer()); return true; case KEYCODE_MOVE_END: ((vt320) buffer).keyPressed(vt320.KEY_END, ' ', getStateForBuffer()); return true; case KEYCODE_PAGE_UP: ((vt320) buffer).keyPressed(vt320.KEY_PAGE_UP, ' ', getStateForBuffer()); return true; case KEYCODE_PAGE_DOWN: ((vt320) buffer).keyPressed(vt320.KEY_PAGE_DOWN, ' ', getStateForBuffer()); 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); } public void sendTab() { try { bridge.transport.write(0x09); } catch (IOException e) { Log.e(TAG, "Problem while trying to send TAB press.", e); try { bridge.transport.flush(); } catch (IOException ioe) { Log.d(TAG, "Our transport was closed, dispatching disconnect event"); bridge.dispatchDisconnect(false); } } } public void sendPressedKey(int key) { ((vt320) buffer).keyPressed(key, ' ', getStateForBuffer()); } /** * @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. *

* 1st press: next key to have meta state
* 2nd press: meta state is locked on
* 3rd press: disable meta state * * @param code */ public void metaPress(int code, boolean forceSticky) { if ((ourMetaState & (code << 1)) != 0) { ourMetaState &= ~(code << 1); } else if ((ourMetaState & code) != 0) { ourMetaState &= ~code; ourMetaState |= code << 1; } else if (forceSticky || (stickyMetas & code) != 0) { ourMetaState |= code; } else { // skip redraw return; } bridge.redraw(); } public void metaPress(int code) { metaPress(code, false); } 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) || PreferenceConstants.STICKY_MODIFIERS.equals(key)) { updatePrefs(); } } private void updatePrefs() { keymode = prefs.getString(PreferenceConstants.KEYMODE, PreferenceConstants.KEYMODE_NONE); shiftedNumbersAreFKeysOnHardKeyboard = prefs.getBoolean(PreferenceConstants.SHIFT_FKEYS, false); controlNumbersAreFKeysOnSoftKeyboard = prefs.getBoolean(PreferenceConstants.CTRL_FKEYS, false); volumeKeysChangeFontSize = prefs.getBoolean(PreferenceConstants.VOLUME_FONT, true); String stickyModifiers = prefs.getString(PreferenceConstants.STICKY_MODIFIERS, PreferenceConstants.NO); if (PreferenceConstants.ALT.equals(stickyModifiers)) { stickyMetas = OUR_ALT_ON; } else if (PreferenceConstants.YES.equals(stickyModifiers)) { stickyMetas = OUR_SHIFT_ON | OUR_CTRL_ON | OUR_ALT_ON; } else { stickyMetas = 0; } } public void setCharset(String encoding) { this.encoding = encoding; } }