diff src/com/five_ten_sg/connectbot/service/TerminalKeyListener.java @ 0:0ce5cc452d02

initial version
author Carl Byington <carl@five-ten-sg.com>
date Thu, 22 May 2014 10:41:19 -0700
parents
children 7ac846a07ed4
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/com/five_ten_sg/connectbot/service/TerminalKeyListener.java	Thu May 22 10:41:19 2014 -0700
@@ -0,0 +1,1198 @@
+/*
+ * 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 com.five_ten_sg.connectbot.service;
+
+import java.io.IOException;
+import java.lang.ref.WeakReference;
+import java.util.List;
+
+import com.five_ten_sg.connectbot.R;
+import com.five_ten_sg.connectbot.TerminalView;
+import com.five_ten_sg.connectbot.bean.SelectionArea;
+import com.five_ten_sg.connectbot.util.PreferenceConstants;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
+import android.content.res.Configuration;
+import android.net.Uri;
+import android.preference.PreferenceManager;
+import android.text.ClipboardManager;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.KeyCharacterMap;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.View.OnKeyListener;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.Toast;
+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";
+
+    public final static int META_CTRL_ON = 0x01;
+    public final static int META_CTRL_LOCK = 0x02;
+    public final static int META_ALT_ON = 0x04;
+    public final static int META_ALT_LOCK = 0x08;
+    public final static int META_SHIFT_ON = 0x10;
+    public final static int META_SHIFT_LOCK = 0x20;
+    public final static int META_SLASH = 0x40;
+    public final static int META_TAB = 0x80;
+
+    // The bit mask of momentary and lock states for each
+    public final static int META_CTRL_MASK = META_CTRL_ON | META_CTRL_LOCK;
+    public final static int META_ALT_MASK = META_ALT_ON | META_ALT_LOCK;
+    public final static int META_SHIFT_MASK = META_SHIFT_ON | META_SHIFT_LOCK;
+
+    // backport constants from api level 11
+    public final static int KEYCODE_ESCAPE = 111;
+    public final static int HC_META_CTRL_ON = 4096;
+    public final static int KEYCODE_PAGE_UP = 92;
+    public final static int KEYCODE_PAGE_DOWN = 93;
+
+    // All the transient key codes
+    public final static int META_TRANSIENT = META_CTRL_ON | META_ALT_ON
+            | META_SHIFT_ON;
+
+    private final TerminalManager manager;
+    private final TerminalBridge bridge;
+    private final vt320 buffer;
+
+    private String keymode = null;
+    private boolean hardKeyboard = false;
+    private String customKeyboard = null;
+
+    private int metaState = 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;
+
+    private Toast debugToast = null;
+    private Toast metakeyToast = null;
+
+    public TerminalKeyListener(TerminalManager manager,
+                               TerminalBridge bridge,
+                               vt320 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);
+        hardKeyboard = (manager.res.getConfiguration().keyboard
+                        == Configuration.KEYBOARD_QWERTY);
+        updateKeymode();
+        updateCustomKeymap();
+    }
+
+    public void sendEscape() {
+        buffer.write(0x1b);
+    }
+
+    /**
+     * Handle onKey() events coming down from a {@link com.five_ten_sg.connectbot.TerminalView} above us.
+     * Modify the keys to make more sense to a host then pass it to the vt320.
+     */
+    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 hardKeyboardHidden = manager.hardKeyboardHidden;
+
+            // Ignore all key-up events except for the special keys
+            if (event.getAction() == KeyEvent.ACTION_UP) {
+                // There's nothing here for virtual keyboard users.
+                if (!hardKeyboard || (hardKeyboard && hardKeyboardHidden))
+                    return false;
+
+                // if keycode debugging enabled, log and print the pressed key
+                if (prefs.getBoolean(PreferenceConstants.DEBUG_KEYCODES, false)) {
+                    String keyCodeString = String.format(": %d", keyCode);
+                    String toastText = v.getContext().getString(R.string.keycode_pressed) + keyCodeString;
+
+                    if (debugToast == null)
+                        debugToast = Toast.makeText(v.getContext(), toastText, Toast.LENGTH_LONG);
+                    else
+                        debugToast.setText(toastText);
+
+                    debugToast.show();
+                }
+
+                if (fullKeyboard()) {
+                    switch (keyCode) {
+                        case KeyEvent.KEYCODE_CTRL_LEFT:
+                        case KeyEvent.KEYCODE_CTRL_RIGHT:
+                            metaKeyUp(META_CTRL_ON);
+                            return true;
+
+                        case KeyEvent.KEYCODE_ALT_LEFT:
+                        case KeyEvent.KEYCODE_ALT_RIGHT:
+                            metaKeyUp(META_ALT_ON);
+                            return true;
+
+                        case KeyEvent.KEYCODE_SHIFT_LEFT:
+                        case KeyEvent.KEYCODE_SHIFT_RIGHT:
+                            metaKeyUp(META_SHIFT_ON);
+                            return true;
+
+                        default:
+                    }
+                }
+                else if (PreferenceConstants.KEYMODE_RIGHT.equals(keymode)) {
+                    if (keyCode == KeyEvent.KEYCODE_ALT_RIGHT
+                            && (metaState & META_SLASH) != 0) {
+                        metaState &= ~(META_SLASH | META_TRANSIENT);
+                        buffer.write('/');
+                        return true;
+                    }
+                    else if (keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT
+                             && (metaState & META_TAB) != 0) {
+                        metaState &= ~(META_TAB | META_TRANSIENT);
+                        buffer.write(0x09);
+                        return true;
+                    }
+                }
+                else if (PreferenceConstants.KEYMODE_LEFT.equals(keymode)) {
+                    if (keyCode == KeyEvent.KEYCODE_ALT_LEFT
+                            && (metaState & META_SLASH) != 0) {
+                        metaState &= ~(META_SLASH | META_TRANSIENT);
+                        buffer.write('/');
+                        return true;
+                    }
+                    else if (keyCode == KeyEvent.KEYCODE_SHIFT_LEFT
+                             && (metaState & META_TAB) != 0) {
+                        metaState &= ~(META_TAB | META_TRANSIENT);
+                        buffer.write(0x09);
+                        return true;
+                    }
+                }
+
+                return false;
+            }
+
+            bridge.resetScrollPosition();
+
+            if (keyCode == KeyEvent.KEYCODE_UNKNOWN &&
+                    event.getAction() == KeyEvent.ACTION_MULTIPLE) {
+                byte[] input = event.getCharacters().getBytes(encoding);
+                buffer.write(input);
+                return true;
+            }
+
+            int curMetaState = event.getMetaState();
+            final int orgMetaState = curMetaState;
+
+            if ((metaState & META_SHIFT_MASK) != 0) {
+                curMetaState |= KeyEvent.META_SHIFT_ON;
+            }
+
+            if ((metaState & META_ALT_MASK) != 0) {
+                curMetaState |= KeyEvent.META_ALT_ON;
+            }
+
+            int uchar = event.getUnicodeChar(curMetaState);
+
+            // no hard keyboard?  ALT-k should pass through to below
+            if ((orgMetaState & KeyEvent.META_ALT_ON) != 0 &&
+                    (!hardKeyboard || hardKeyboardHidden)) {
+                uchar = 0;
+            }
+
+            if ((uchar & KeyCharacterMap.COMBINING_ACCENT) != 0) {
+                mDeadKey = uchar & KeyCharacterMap.COMBINING_ACCENT_MASK;
+                return true;
+            }
+
+            if (mDeadKey != 0 && uchar != 0) {
+                uchar = KeyCharacterMap.getDeadChar(mDeadKey, uchar);
+                mDeadKey = 0;
+            }
+
+            // handle customized keymaps
+            if (customKeymapAction(v, keyCode, event))
+                return true;
+
+            if (v != null) {
+                //Show up the CharacterPickerDialog when the SYM key is pressed
+                if ((isSymKey(keyCode) || uchar == KeyCharacterMap.PICKER_DIALOG_INPUT)) {
+                    bridge.showCharPickerDialog();
+
+                    if (metaState == 4) { // reset fn-key state
+                        metaState = 0;
+                        bridge.redraw();
+                    }
+
+                    return true;
+                }
+                else if (keyCode == KeyEvent.KEYCODE_SEARCH) {
+                    //Show up the URL scan dialog when the search key is pressed
+                    urlScan(v);
+                    return true;
+                }
+            }
+
+            // otherwise pass through to existing session
+            // print normal keys
+            if (uchar > 0x00 && keyCode != KeyEvent.KEYCODE_ENTER) {
+                metaState &= ~(META_SLASH | META_TAB);
+                // Remove shift and alt modifiers
+                final int lastMetaState = metaState;
+                metaState &= ~(META_SHIFT_ON | META_ALT_ON);
+
+                if (metaState != lastMetaState) {
+                    bridge.redraw();
+                }
+
+                if ((metaState & META_CTRL_MASK) != 0) {
+                    metaState &= ~META_CTRL_ON;
+                    bridge.redraw();
+
+                    // If there is no hard keyboard or there is a hard keyboard currently hidden,
+                    // CTRL-1 through CTRL-9 will send F1 through F9
+                    if ((!hardKeyboard || (hardKeyboard && hardKeyboardHidden))
+                            && sendFunctionKey(keyCode))
+                        return true;
+
+                    uchar = keyAsControl(uchar);
+                }
+
+                // handle pressing f-keys
+                if ((hardKeyboard && !hardKeyboardHidden)
+                        && (curMetaState & KeyEvent.META_ALT_ON) != 0
+                        && (curMetaState & KeyEvent.META_SHIFT_ON) != 0
+                        && sendFunctionKey(keyCode))
+                    return true;
+
+                if (uchar < 0x80)
+                    buffer.write(uchar);
+                else
+                    // TODO write encoding routine that doesn't allocate each time
+                    buffer.write(new String(Character.toChars(uchar))
+                                 .getBytes(encoding));
+
+                return true;
+            }
+
+            // send ctrl and meta-keys as appropriate
+            if (!hardKeyboard || hardKeyboardHidden) {
+                int k = event.getUnicodeChar(0);
+                int k0 = k;
+                boolean sendCtrl = false;
+                boolean sendMeta = false;
+
+                if (k != 0) {
+                    if ((orgMetaState & HC_META_CTRL_ON) != 0) {
+                        k = keyAsControl(k);
+
+                        if (k != k0)
+                            sendCtrl = true;
+
+                        // send F1-F10 via CTRL-1 through CTRL-0
+                        if (!sendCtrl && sendFunctionKey(keyCode))
+                            return true;
+                    }
+                    else if ((orgMetaState & KeyEvent.META_ALT_ON) != 0) {
+                        sendMeta = true;
+                        buffer.write(0x1b);
+                    }
+
+                    if (sendMeta || sendCtrl) {
+                        buffer.write(k);
+                        return true;
+                    }
+                }
+            }
+
+            // handle meta and f-keys for full hardware keyboard
+            if (hardKeyboard && !hardKeyboardHidden && fullKeyboard()) {
+                int k = event.getUnicodeChar(orgMetaState & KeyEvent.META_SHIFT_ON);
+                int k0 = k;
+
+                if (k != 0) {
+                    if ((orgMetaState & HC_META_CTRL_ON) != 0) {
+                        k = keyAsControl(k);
+
+                        if (k != k0)
+                            buffer.write(k);
+
+                        return true;
+                    }
+                    else if ((orgMetaState & KeyEvent.META_ALT_ON) != 0) {
+                        buffer.write(0x1b);
+                        buffer.write(k);
+                        return true;
+                    }
+                }
+
+                if (sendFullSpecialKey(keyCode))
+                    return true;
+            }
+
+            // try handling keymode shortcuts
+            if (hardKeyboard && !hardKeyboardHidden &&
+                    event.getRepeatCount() == 0) {
+                if (PreferenceConstants.KEYMODE_RIGHT.equals(keymode)) {
+                    switch (keyCode) {
+                        case KeyEvent.KEYCODE_ALT_RIGHT:
+                            metaState |= META_SLASH;
+                            return true;
+
+                        case KeyEvent.KEYCODE_SHIFT_RIGHT:
+                            metaState |= META_TAB;
+                            return true;
+
+                        case KeyEvent.KEYCODE_SHIFT_LEFT:
+                            metaPress(META_SHIFT_ON);
+                            return true;
+
+                        case KeyEvent.KEYCODE_ALT_LEFT:
+                            metaPress(META_ALT_ON);
+                            return true;
+                    }
+                }
+                else if (PreferenceConstants.KEYMODE_LEFT.equals(keymode)) {
+                    switch (keyCode) {
+                        case KeyEvent.KEYCODE_ALT_LEFT:
+                            metaState |= META_SLASH;
+                            return true;
+
+                        case KeyEvent.KEYCODE_SHIFT_LEFT:
+                            metaState |= META_TAB;
+                            return true;
+
+                        case KeyEvent.KEYCODE_SHIFT_RIGHT:
+                            metaPress(META_SHIFT_ON);
+                            return true;
+
+                        case KeyEvent.KEYCODE_ALT_RIGHT:
+                            metaPress(META_ALT_ON);
+                            return true;
+                    }
+                }
+                else {
+                    switch (keyCode) {
+                        case KeyEvent.KEYCODE_ALT_RIGHT:
+                        case KeyEvent.KEYCODE_ALT_LEFT:
+                            metaPress(META_ALT_ON);
+                            return true;
+
+                        case KeyEvent.KEYCODE_SHIFT_LEFT:
+                        case KeyEvent.KEYCODE_SHIFT_RIGHT:
+                            metaPress(META_SHIFT_ON);
+                            return true;
+                    }
+                }
+
+                // Handle hardware CTRL keys
+                if (keyCode == KeyEvent.KEYCODE_CTRL_LEFT ||
+                        keyCode == KeyEvent.KEYCODE_CTRL_RIGHT) {
+                    ctrlKeySpecial();
+                    return true;
+                }
+            }
+
+            // look for special chars
+            switch (keyCode) {
+                case KEYCODE_ESCAPE:
+                    buffer.write(0x1b);
+                    return true;
+
+                case KeyEvent.KEYCODE_TAB:
+                    buffer.write(0x09);
+                    return true;
+
+                case KEYCODE_PAGE_DOWN:
+                    buffer.keyPressed(vt320.KEY_PAGE_DOWN, ' ', getStateForBuffer());
+                    metaState &= ~META_TRANSIENT;
+                    bridge.tryKeyVibrate();
+                    return true;
+
+                case KEYCODE_PAGE_UP:
+                    buffer.keyPressed(vt320.KEY_PAGE_UP, ' ', getStateForBuffer());
+                    metaState &= ~META_TRANSIENT;
+                    bridge.tryKeyVibrate();
+                    return true;
+
+                case KeyEvent.KEYCODE_MOVE_HOME:
+                    buffer.keyPressed(vt320.KEY_HOME, ' ', getStateForBuffer());
+                    metaState &= ~META_TRANSIENT;
+                    bridge.tryKeyVibrate();
+                    return true;
+
+                case KeyEvent.KEYCODE_MOVE_END:
+                    buffer.keyPressed(vt320.KEY_END, ' ', getStateForBuffer());
+                    metaState &= ~META_TRANSIENT;
+                    bridge.tryKeyVibrate();
+                    return true;
+
+                case KeyEvent.KEYCODE_CAMERA:
+                    // check to see which shortcut the camera button triggers
+                    String hwbuttonShortcut = manager.prefs.getString(
+                                                  PreferenceConstants.CAMERA,
+                                                  PreferenceConstants.HWBUTTON_SCREEN_CAPTURE);
+                    return (handleShortcut(v, hwbuttonShortcut));
+
+                case KeyEvent.KEYCODE_VOLUME_UP:
+                    // check to see which shortcut the camera button triggers
+                    hwbuttonShortcut = manager.prefs.getString(
+                                           PreferenceConstants.VOLUP,
+                                           PreferenceConstants.HWBUTTON_CTRL);
+                    return (handleShortcut(v, hwbuttonShortcut));
+
+                case KeyEvent.KEYCODE_VOLUME_DOWN:
+                    // check to see which shortcut the camera button triggers
+                    hwbuttonShortcut = manager.prefs.getString(
+                                           PreferenceConstants.VOLDN,
+                                           PreferenceConstants.HWBUTTON_TAB);
+                    return (handleShortcut(v, hwbuttonShortcut));
+
+                case KeyEvent.KEYCODE_SEARCH:
+                    // check to see which shortcut the camera button triggers
+                    hwbuttonShortcut = manager.prefs.getString(
+                                           PreferenceConstants.SEARCH,
+                                           PreferenceConstants.HWBUTTON_ESC);
+                    return (handleShortcut(v, hwbuttonShortcut));
+
+                case KeyEvent.KEYCODE_DEL:
+                    if ((metaState & META_ALT_MASK) != 0) {
+                        buffer.keyPressed(vt320.KEY_INSERT, ' ',
+                                          getStateForBuffer());
+                    }
+                    else {
+                        buffer.keyPressed(vt320.KEY_BACK_SPACE, ' ',
+                                          getStateForBuffer());
+                    }
+
+                    metaState &= ~META_TRANSIENT;
+                    return true;
+
+                case KeyEvent.KEYCODE_ENTER:
+                    buffer.write('\r');
+                    metaState &= ~META_TRANSIENT;
+                    return true;
+
+                case KeyEvent.KEYCODE_DPAD_LEFT:
+                    if (selectingForCopy) {
+                        selectionArea.decrementColumn();
+                        bridge.redraw();
+                    }
+                    else {
+                        if ((metaState & META_ALT_MASK) != 0) {
+                            buffer.keyPressed(vt320.KEY_HOME, ' ',
+                                              getStateForBuffer());
+                        }
+                        else {
+                            buffer.keyPressed(vt320.KEY_LEFT, ' ',
+                                              getStateForBuffer());
+                        }
+
+                        metaState &= ~META_TRANSIENT;
+                        bridge.tryKeyVibrate();
+                    }
+
+                    return true;
+
+                case KeyEvent.KEYCODE_DPAD_UP:
+                    if (selectingForCopy) {
+                        selectionArea.decrementRow();
+                        bridge.redraw();
+                    }
+                    else {
+                        if ((metaState & META_ALT_MASK) != 0) {
+                            buffer.keyPressed(vt320.KEY_PAGE_UP, ' ',
+                                              getStateForBuffer());
+                        }
+                        else {
+                            buffer.keyPressed(vt320.KEY_UP, ' ',
+                                              getStateForBuffer());
+                        }
+
+                        metaState &= ~META_TRANSIENT;
+                        bridge.tryKeyVibrate();
+                    }
+
+                    return true;
+
+                case KeyEvent.KEYCODE_DPAD_DOWN:
+                    if (selectingForCopy) {
+                        selectionArea.incrementRow();
+                        bridge.redraw();
+                    }
+                    else {
+                        if ((metaState & META_ALT_MASK) != 0) {
+                            buffer.keyPressed(vt320.KEY_PAGE_DOWN, ' ',
+                                              getStateForBuffer());
+                        }
+                        else {
+                            buffer.keyPressed(vt320.KEY_DOWN, ' ',
+                                              getStateForBuffer());
+                        }
+
+                        metaState &= ~META_TRANSIENT;
+                        bridge.tryKeyVibrate();
+                    }
+
+                    return true;
+
+                case KeyEvent.KEYCODE_DPAD_RIGHT:
+                    if (selectingForCopy) {
+                        selectionArea.incrementColumn();
+                        bridge.redraw();
+                    }
+                    else {
+                        if ((metaState & META_ALT_MASK) != 0) {
+                            buffer.keyPressed(vt320.KEY_END, ' ',
+                                              getStateForBuffer());
+                        }
+                        else {
+                            buffer.keyPressed(vt320.KEY_RIGHT, ' ',
+                                              getStateForBuffer());
+                        }
+
+                        metaState &= ~META_TRANSIENT;
+                        bridge.tryKeyVibrate();
+                    }
+
+                    return true;
+
+                case KeyEvent.KEYCODE_DPAD_CENTER:
+                    ctrlKeySpecial();
+                    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;
+    }
+
+    private boolean handleShortcut(View v, String shortcut) {
+        if (PreferenceConstants.HWBUTTON_SCREEN_CAPTURE.equals(shortcut)) {
+            bridge.captureScreen();
+        }
+        else if (PreferenceConstants.HWBUTTON_CTRL.equals(shortcut)) {
+            showMetakeyToast(v, PreferenceConstants.HWBUTTON_CTRL);
+            metaPress(META_CTRL_ON);
+        }
+        else if (PreferenceConstants.HWBUTTON_TAB.equals(shortcut)) {
+            buffer.write(0x09);
+        }
+        else if (PreferenceConstants.HWBUTTON_CTRLA_SPACE.equals(shortcut)) {
+            buffer.write(0x01);
+            buffer.write(' ');
+        }
+        else if (PreferenceConstants.HWBUTTON_CTRLA.equals(shortcut)) {
+            buffer.write(0x01);
+        }
+        else if (PreferenceConstants.HWBUTTON_ESC.equals(shortcut)) {
+            showMetakeyToast(v, PreferenceConstants.HWBUTTON_ESC);
+            buffer.write(0x1b);
+        }
+        else if (PreferenceConstants.HWBUTTON_ESC_A.equals(shortcut)) {
+            buffer.write(0x1b);
+            buffer.write('a');
+        }
+        else {
+            return (false);
+        }
+
+        return (true);
+    }
+
+    private void showMetakeyToast(View v, String keyname) {
+        if (metakeyToast == null)
+            metakeyToast = Toast.makeText(v.getContext(), keyname, Toast.LENGTH_LONG);
+        else
+            metakeyToast.setText(keyname);
+
+        metakeyToast.setGravity(Gravity.TOP | Gravity.RIGHT, 0, 0);
+        metakeyToast.show();
+    }
+
+    public int keyAsControl(int key) {
+        // Support CTRL-a through CTRL-z
+        if (key >= 0x60 && key <= 0x7A)
+            key -= 0x60;
+        // Support CTRL-A through CTRL-_
+        else if (key >= 0x40 && 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;
+    }
+
+    /**
+     * @param key
+     * @return successful
+     */
+    private boolean sendFunctionKey(int keyCode) {
+        switch (keyCode) {
+            case KeyEvent.KEYCODE_1:
+                buffer.keyPressed(vt320.KEY_F1, ' ', 0);
+                return true;
+
+            case KeyEvent.KEYCODE_2:
+                buffer.keyPressed(vt320.KEY_F2, ' ', 0);
+                return true;
+
+            case KeyEvent.KEYCODE_3:
+                buffer.keyPressed(vt320.KEY_F3, ' ', 0);
+                return true;
+
+            case KeyEvent.KEYCODE_4:
+                buffer.keyPressed(vt320.KEY_F4, ' ', 0);
+                return true;
+
+            case KeyEvent.KEYCODE_5:
+                buffer.keyPressed(vt320.KEY_F5, ' ', 0);
+                return true;
+
+            case KeyEvent.KEYCODE_6:
+                buffer.keyPressed(vt320.KEY_F6, ' ', 0);
+                return true;
+
+            case KeyEvent.KEYCODE_7:
+                buffer.keyPressed(vt320.KEY_F7, ' ', 0);
+                return true;
+
+            case KeyEvent.KEYCODE_8:
+                buffer.keyPressed(vt320.KEY_F8, ' ', 0);
+                return true;
+
+            case KeyEvent.KEYCODE_9:
+                buffer.keyPressed(vt320.KEY_F9, ' ', 0);
+                return true;
+
+            case KeyEvent.KEYCODE_0:
+                buffer.keyPressed(vt320.KEY_F10, ' ', 0);
+                return true;
+
+            default:
+                return false;
+        }
+    }
+
+    private boolean sendFullSpecialKey(int keyCode) {
+        switch (keyCode) {
+            case KeyEvent.KEYCODE_F1:
+                buffer.keyPressed(vt320.KEY_F1, ' ', 0);
+                return true;
+
+            case KeyEvent.KEYCODE_F2:
+                buffer.keyPressed(vt320.KEY_F2, ' ', 0);
+                return true;
+
+            case KeyEvent.KEYCODE_F3:
+                buffer.keyPressed(vt320.KEY_F3, ' ', 0);
+                return true;
+
+            case KeyEvent.KEYCODE_F4:
+                buffer.keyPressed(vt320.KEY_F4, ' ', 0);
+                return true;
+
+            case KeyEvent.KEYCODE_F5:
+                buffer.keyPressed(vt320.KEY_F5, ' ', 0);
+                return true;
+
+            case KeyEvent.KEYCODE_F6:
+                buffer.keyPressed(vt320.KEY_F6, ' ', 0);
+                return true;
+
+            case KeyEvent.KEYCODE_F7:
+                buffer.keyPressed(vt320.KEY_F7, ' ', 0);
+                return true;
+
+            case KeyEvent.KEYCODE_F8:
+                buffer.keyPressed(vt320.KEY_F8, ' ', 0);
+                return true;
+
+            case KeyEvent.KEYCODE_F9:
+                buffer.keyPressed(vt320.KEY_F9, ' ', 0);
+                return true;
+
+            case KeyEvent.KEYCODE_F10:
+                buffer.keyPressed(vt320.KEY_F10, ' ', 0);
+                return true;
+
+            case KeyEvent.KEYCODE_F11:
+                buffer.keyPressed(vt320.KEY_F10, ' ', 0);
+                return true;
+
+            case KeyEvent.KEYCODE_F12:
+                buffer.keyPressed(vt320.KEY_F10, ' ', 0);
+                return true;
+
+            case KeyEvent.KEYCODE_INSERT:
+                buffer.keyPressed(vt320.KEY_INSERT, ' ', 0);
+                return true;
+
+            case KeyEvent.KEYCODE_FORWARD_DEL:
+                buffer.keyPressed(vt320.KEY_DELETE, ' ', 0);
+                return true;
+
+            /*
+                    case KeyEvent.KEYCODE_PAGE_UP:
+                        buffer.keyPressed(vt320.KEY_PAGE_UP, ' ', 0);
+                        return true;
+                    case KeyEvent.KEYCODE_PAGE_DOWN:
+                        buffer.keyPressed(vt320.KEY_PAGE_DOWN, ' ', 0);
+                        return true;
+                    case KeyEvent.KEYCODE_MOVE_HOME:
+                        buffer.keyPressed(vt320.KEY_HOME, ' ', getStateForBuffer());
+                        return true;
+                    case KeyEvent.KEYCODE_MOVE_END:
+                        buffer.keyPressed(vt320.KEY_END, ' ', getStateForBuffer());
+                        return true;
+            */
+            default:
+                return false;
+        }
+    }
+
+    /**
+     * Handle meta key presses for full hardware keyboard
+     */
+    private void metaKeyDown(int code) {
+        if ((metaState & code) == 0) {
+            metaState |= code;
+            bridge.redraw();
+        }
+    }
+
+    private void metaKeyUp(int code) {
+        if ((metaState & code) != 0) {
+            metaState &= ~code;
+            bridge.redraw();
+        }
+    }
+
+    /**
+     * 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 ((metaState & (code << 1)) != 0) {
+            metaState &= ~(code << 1);
+        }
+        else if ((metaState & code) != 0) {
+            metaState &= ~code;
+
+            if (!fullKeyboard())
+                metaState |= code << 1;
+        }
+        else
+            metaState |= code;
+
+        bridge.redraw();
+    }
+
+    public void setTerminalKeyMode(String keymode) {
+        this.keymode = keymode;
+    }
+
+    private int getStateForBuffer() {
+        int bufferState = 0;
+
+        if ((metaState & META_CTRL_MASK) != 0)
+            bufferState |= vt320.KEY_CONTROL;
+
+        if ((metaState & META_SHIFT_MASK) != 0)
+            bufferState |= vt320.KEY_SHIFT;
+
+        if ((metaState & META_ALT_MASK) != 0)
+            bufferState |= vt320.KEY_ALT;
+
+        return bufferState;
+    }
+
+    public int getMetaState() {
+        return metaState;
+    }
+
+    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)) {
+            updateKeymode();
+        }
+        else if (PreferenceConstants.CUSTOM_KEYMAP.equals(key)) {
+            updateCustomKeymap();
+        }
+    }
+
+    private void updateKeymode() {
+        keymode = prefs.getString(PreferenceConstants.KEYMODE, PreferenceConstants.KEYMODE_RIGHT);
+    }
+
+    private void updateCustomKeymap() {
+        customKeyboard = prefs.getString(PreferenceConstants.CUSTOM_KEYMAP,
+                                         PreferenceConstants.CUSTOM_KEYMAP_DISABLED);
+    }
+
+    public void setCharset(String encoding) {
+        this.encoding = encoding;
+    }
+
+
+
+    private void ctrlKeySpecial() {
+        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 ((metaState & META_CTRL_ON) != 0) {
+                buffer.write(0x1b);
+                metaState &= ~META_CTRL_ON;
+            }
+            else
+                metaPress(META_CTRL_ON);
+        }
+
+        bridge.redraw();
+    }
+
+    private boolean customKeymapAction(View v, int keyCode, KeyEvent event) {
+        if (bridge == null || customKeyboard.equals(PreferenceConstants.CUSTOM_KEYMAP_DISABLED))
+            return false;
+
+        byte c = 0x00;
+        int termKey = 0;
+
+        if (fullKeyboard()) {
+            switch (keyCode) {
+                case KeyEvent.KEYCODE_CTRL_LEFT:
+                case KeyEvent.KEYCODE_CTRL_RIGHT:
+                    metaKeyDown(META_CTRL_ON);
+                    return true;
+
+                case KeyEvent.KEYCODE_ALT_LEFT:
+                case KeyEvent.KEYCODE_ALT_RIGHT:
+                    metaKeyDown(META_ALT_ON);
+                    return true;
+
+                case KeyEvent.KEYCODE_SHIFT_LEFT:
+                case KeyEvent.KEYCODE_SHIFT_RIGHT:
+                    metaKeyDown(META_SHIFT_ON);
+                    return true;
+
+                case KeyEvent.KEYCODE_BACK:
+                    if (customKeyboard.equals(PreferenceConstants.CUSTOM_KEYMAP_ASUS_TF)) {
+                        // Check to see whether this is the back button on the
+                        // screen (-1) or the Asus Transformer Keyboard Dock.
+                        // Treat the HW button as ESC.
+                        if (event.getDeviceId() > 0) {
+                            buffer.write(0x1b);
+                            return true;
+                        }
+                    }
+
+                default:
+            }
+        }
+
+        if (customKeyboard.equals(PreferenceConstants.CUSTOM_KEYMAP_ASUS_TF)) {
+            if ((metaState & META_ALT_MASK) != 0
+                    && (metaState & META_SHIFT_MASK) != 0
+                    && sendFunctionKey(keyCode))
+                return true;
+        }
+        else if (customKeyboard.equals(PreferenceConstants.CUSTOM_KEYMAP_SE_XPPRO)) {
+            // Sony Ericsson Xperia pro (MK16i) and Xperia mini Pro (SK17i)
+            // Language key acts as CTRL
+            if (keyCode == KeyEvent.KEYCODE_SWITCH_CHARSET) {
+                ctrlKeySpecial();
+                return true;
+            }
+
+            if ((metaState & META_ALT_MASK) != 0) {
+                if ((metaState & META_SHIFT_MASK) != 0) {
+                    // ALT + shift + key
+                    switch (keyCode) {
+                        case KeyEvent.KEYCODE_U:
+                            c = 0x5B;
+                            break;
+
+                        case KeyEvent.KEYCODE_I:
+                            c = 0x5D;
+                            break;
+
+                        case KeyEvent.KEYCODE_O:
+                            c = 0x7B;
+                            break;
+
+                        case KeyEvent.KEYCODE_P:
+                            c = 0x7D;
+                            break;
+                    }
+                }
+                else {
+                    // ALT + key
+                    switch (keyCode) {
+                        case KeyEvent.KEYCODE_S:
+                            c = 0x7c;
+                            break;
+
+                        case KeyEvent.KEYCODE_Z:
+                            c = 0x5c;
+                            break;
+
+                        case KeyEvent.KEYCODE_DEL:
+                            termKey = vt320.KEY_DELETE;
+                            break;
+                    }
+                }
+            }
+            else if ((metaState & META_SHIFT_MASK) != 0) {
+                // shift + key
+                switch (keyCode) {
+                    case KeyEvent.KEYCODE_AT:
+                        c = 0x3c;
+                        break;
+
+                    case KeyEvent.KEYCODE_COMMA:
+                        c = 0x3e;
+                        break;
+
+                    case KeyEvent.KEYCODE_PERIOD:
+                        c = 0x5e;
+                        break;
+
+                    case KeyEvent.KEYCODE_GRAVE:
+                        c = 0x60;
+                        break;
+
+                    case KeyEvent.KEYCODE_APOSTROPHE:
+                        c = 0x7e;
+                        break;
+
+                    case KeyEvent.KEYCODE_DEL:
+                        termKey = vt320.KEY_BACK_SPACE;
+                        break;
+                }
+            }
+        }
+        else if (customKeyboard.equals(PreferenceConstants.CUSTOM_KEYMAP_SGH_I927)) {
+            // Samsung Captivate Glide (SGH-i927)
+            if (keyCode == 115) {
+                // .com key = ESC
+                c = 0x1b;
+                return true;
+            }
+            else if (keyCode == 116) {
+                // Microphone key = TAB
+                c = 0x09;
+            }
+            else if ((metaState & META_ALT_MASK) != 0 && (metaState & META_SHIFT_MASK) != 0) {
+                switch (keyCode) {
+                    case KeyEvent.KEYCODE_O:
+                        c = 0x5B;
+                        break;
+
+                    case KeyEvent.KEYCODE_P:
+                        c = 0x5D;
+                        break;
+
+                    case KeyEvent.KEYCODE_A:
+                        c = 0x3C;
+                        break;
+
+                    case KeyEvent.KEYCODE_D:
+                        c = 0x3E;
+                        break;
+                }
+            }
+        }
+        else if (customKeyboard.equals(PreferenceConstants.CUSTOM_KEYMAP_SGH_I927_ICS)) {
+            // Samsung Captivate Glide (SGH-i927) Ice Cream Sandwich (4.0.x)
+            if (keyCode == 226) {
+                // .com key = ESC
+                c = 0x1b;
+            }
+            else if (keyCode == 220) {
+                // Microphone key = TAB
+                c = 0x09;
+            }
+            else if ((metaState & META_ALT_MASK) != 0 && (metaState & META_SHIFT_MASK) != 0) {
+                switch (keyCode) {
+                    case KeyEvent.KEYCODE_O:
+                        c = 0x5B;
+                        break;
+
+                    case KeyEvent.KEYCODE_P:
+                        c = 0x5D;
+                        break;
+
+                    case KeyEvent.KEYCODE_A:
+                        c = 0x3C;
+                        break;
+
+                    case KeyEvent.KEYCODE_D:
+                        c = 0x3E;
+                        break;
+                }
+            }
+        }
+
+        if ((c != 0x00) || termKey != 0) {
+            if (c != 0x00)
+                buffer.write(c);
+            else
+                buffer.keyPressed(termKey, ' ', 0);
+
+            metaState &= ~(META_SHIFT_ON | META_ALT_ON);
+            bridge.redraw();
+            return true;
+        }
+
+        return false;
+    }
+
+    public void urlScan(View v) {
+        //final TerminalView terminalView = (TerminalView) findCurrentView(R.id.console_flip);
+        List<String> urls = bridge.scanForURLs();
+        Dialog urlDialog = new Dialog(v.getContext());
+        urlDialog.setTitle(R.string.console_menu_urlscan);
+        ListView urlListView = new ListView(v.getContext());
+        URLItemListener urlListener = new URLItemListener(v.getContext());
+        urlListView.setOnItemClickListener(urlListener);
+        urlListView.setAdapter(new ArrayAdapter<String> (v.getContext(), android.R.layout.simple_list_item_1, urls));
+        urlDialog.setContentView(urlListView);
+        urlDialog.show();
+    }
+
+    public boolean isSymKey(int keyCode) {
+        if (keyCode == KeyEvent.KEYCODE_SYM ||
+                keyCode == KeyEvent.KEYCODE_PICTSYMBOLS)
+            return true;
+
+        if (customKeyboard.equals(PreferenceConstants.CUSTOM_KEYMAP_SGH_I927_ICS) &&
+                keyCode == 227)
+            return true;
+
+        return false;
+    }
+
+    private boolean fullKeyboard() {
+        if (customKeyboard.equals(PreferenceConstants.CUSTOM_KEYMAP_FULL) ||
+                (customKeyboard.equals(PreferenceConstants.CUSTOM_KEYMAP_ASUS_TF)))
+            return true;
+
+        return false;
+    }
+
+    private class URLItemListener implements OnItemClickListener {
+        private WeakReference<Context> contextRef;
+
+        URLItemListener(Context context) {
+            this.contextRef = new WeakReference<Context> (context);
+        }
+
+        public void onItemClick(AdapterView<?> arg0, View view, int position,
+                                long id) {
+            Context context = contextRef.get();
+
+            if (context == null)
+                return;
+
+            try {
+                TextView urlView = (TextView) view;
+                String url = urlView.getText().toString();
+
+                if (url.indexOf("://") < 0)
+                    url = "http://" + url;
+
+                Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
+                context.startActivity(intent);
+            }
+            catch (Exception e) {
+                Log.e(TAG, "couldn't open URL", e);
+                // We should probably tell the user that we couldn't find a
+                // handler...
+            }
+        }
+    }
+}
+