Home | History | Annotate | Download | only in service
      1 /*
      2  * ConnectBot: simple, powerful, open-source SSH client for Android
      3  * Copyright 2010 Kenny Root, Jeffrey Sharkey
      4  *
      5  * Licensed under the Apache License, Version 2.0 (the "License");
      6  * you may not use this file except in compliance with the License.
      7  * You may obtain a copy of the License at
      8  *
      9  *     http://www.apache.org/licenses/LICENSE-2.0
     10  *
     11  * Unless required by applicable law or agreed to in writing, software
     12  * distributed under the License is distributed on an "AS IS" BASIS,
     13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     14  * See the License for the specific language governing permissions and
     15  * limitations under the License.
     16  */
     17 package org.connectbot.service;
     18 
     19 import android.content.SharedPreferences;
     20 import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
     21 import android.content.res.Configuration;
     22 import android.text.ClipboardManager;
     23 import android.view.KeyCharacterMap;
     24 import android.view.KeyEvent;
     25 import android.view.View;
     26 import android.view.View.OnKeyListener;
     27 
     28 import com.googlecode.android_scripting.Log;
     29 
     30 import de.mud.terminal.VDUBuffer;
     31 import de.mud.terminal.vt320;
     32 
     33 import java.io.IOException;
     34 
     35 import org.connectbot.TerminalView;
     36 import org.connectbot.transport.AbsTransport;
     37 import org.connectbot.util.PreferenceConstants;
     38 import org.connectbot.util.SelectionArea;
     39 
     40 /**
     41  */
     42 public class TerminalKeyListener implements OnKeyListener, OnSharedPreferenceChangeListener {
     43 
     44   public final static int META_CTRL_ON = 0x01;
     45   public final static int META_CTRL_LOCK = 0x02;
     46   public final static int META_ALT_ON = 0x04;
     47   public final static int META_ALT_LOCK = 0x08;
     48   public final static int META_SHIFT_ON = 0x10;
     49   public final static int META_SHIFT_LOCK = 0x20;
     50   public final static int META_SLASH = 0x40;
     51   public final static int META_TAB = 0x80;
     52 
     53   // The bit mask of momentary and lock states for each
     54   public final static int META_CTRL_MASK = META_CTRL_ON | META_CTRL_LOCK;
     55   public final static int META_ALT_MASK = META_ALT_ON | META_ALT_LOCK;
     56   public final static int META_SHIFT_MASK = META_SHIFT_ON | META_SHIFT_LOCK;
     57 
     58   // All the transient key codes
     59   public final static int META_TRANSIENT = META_CTRL_ON | META_ALT_ON | META_SHIFT_ON;
     60 
     61   public final static int KEYBOARD_META_CTRL_ON = 0x1000; // Ctrl key mask for API 11+
     62   private final TerminalManager manager;
     63   private final TerminalBridge bridge;
     64   private final VDUBuffer buffer;
     65 
     66   protected KeyCharacterMap keymap = KeyCharacterMap.load(KeyCharacterMap.BUILT_IN_KEYBOARD);
     67 
     68   private String keymode = null;
     69   private boolean hardKeyboard = false;
     70 
     71   private int metaState = 0;
     72 
     73   private ClipboardManager clipboard = null;
     74   private boolean selectingForCopy = false;
     75   private final SelectionArea selectionArea;
     76 
     77   private String encoding;
     78 
     79   public TerminalKeyListener(TerminalManager manager, TerminalBridge bridge, VDUBuffer buffer,
     80       String encoding) {
     81     this.manager = manager;
     82     this.bridge = bridge;
     83     this.buffer = buffer;
     84     this.encoding = encoding;
     85 
     86     selectionArea = new SelectionArea();
     87 
     88     manager.registerOnSharedPreferenceChangeListener(this);
     89 
     90     hardKeyboard =
     91         (manager.getResources().getConfiguration().keyboard == Configuration.KEYBOARD_QWERTY);
     92 
     93     updateKeymode();
     94   }
     95 
     96   /**
     97    * Handle onKey() events coming down from a {@link TerminalView} above us. Modify the keys to make
     98    * more sense to a host then pass it to the transport.
     99    */
    100   public boolean onKey(View v, int keyCode, KeyEvent event) {
    101     try {
    102       final boolean hardKeyboardHidden = manager.isHardKeyboardHidden();
    103 
    104       AbsTransport transport = bridge.getTransport();
    105 
    106       // Ignore all key-up events except for the special keys
    107       if (event.getAction() == KeyEvent.ACTION_UP) {
    108         // There's nothing here for virtual keyboard users.
    109         if (!hardKeyboard || (hardKeyboard && hardKeyboardHidden)) {
    110           return false;
    111         }
    112 
    113         // skip keys if we aren't connected yet or have been disconnected
    114         if (transport == null || !transport.isSessionOpen()) {
    115           return false;
    116         }
    117 
    118         if (PreferenceConstants.KEYMODE_RIGHT.equals(keymode)) {
    119           if (keyCode == KeyEvent.KEYCODE_ALT_RIGHT && (metaState & META_SLASH) != 0) {
    120             metaState &= ~(META_SLASH | META_TRANSIENT);
    121             transport.write('/');
    122             return true;
    123           } else if (keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT && (metaState & META_TAB) != 0) {
    124             metaState &= ~(META_TAB | META_TRANSIENT);
    125             transport.write(0x09);
    126             return true;
    127           }
    128         } else if (PreferenceConstants.KEYMODE_LEFT.equals(keymode)) {
    129           if (keyCode == KeyEvent.KEYCODE_ALT_LEFT && (metaState & META_SLASH) != 0) {
    130             metaState &= ~(META_SLASH | META_TRANSIENT);
    131             transport.write('/');
    132             return true;
    133           } else if (keyCode == KeyEvent.KEYCODE_SHIFT_LEFT && (metaState & META_TAB) != 0) {
    134             metaState &= ~(META_TAB | META_TRANSIENT);
    135             transport.write(0x09);
    136             return true;
    137           }
    138         }
    139 
    140         return false;
    141       }
    142 
    143       if (keyCode == KeyEvent.KEYCODE_BACK && transport != null) {
    144         bridge.dispatchDisconnect(!transport.isSessionOpen());
    145         return true;
    146       }
    147 
    148       // check for terminal resizing keys
    149       if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
    150         bridge.increaseFontSize();
    151         return true;
    152       } else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
    153         bridge.decreaseFontSize();
    154         return true;
    155       }
    156 
    157       // skip keys if we aren't connected yet or have been disconnected
    158       if (transport == null || !transport.isSessionOpen()) {
    159         return false;
    160       }
    161 
    162       bridge.resetScrollPosition();
    163 
    164       boolean printing = (keymap.isPrintingKey(keyCode) || keyCode == KeyEvent.KEYCODE_SPACE);
    165 
    166       // otherwise pass through to existing session
    167       // print normal keys
    168       if (printing) {
    169         int curMetaState = event.getMetaState();
    170 
    171         metaState &= ~(META_SLASH | META_TAB);
    172 
    173         if ((metaState & META_SHIFT_MASK) != 0) {
    174           curMetaState |= KeyEvent.META_SHIFT_ON;
    175           metaState &= ~META_SHIFT_ON;
    176           bridge.redraw();
    177         }
    178 
    179         if ((metaState & META_ALT_MASK) != 0) {
    180           curMetaState |= KeyEvent.META_ALT_ON;
    181           metaState &= ~META_ALT_ON;
    182           bridge.redraw();
    183         }
    184 
    185         int key = keymap.get(keyCode, curMetaState);
    186         if ((curMetaState & KEYBOARD_META_CTRL_ON) != 0) {
    187           metaState |= META_CTRL_ON;
    188           key = keymap.get(keyCode, 0);
    189         }
    190 
    191         if ((metaState & META_CTRL_MASK) != 0) {
    192           metaState &= ~META_CTRL_ON;
    193           bridge.redraw();
    194 
    195           if ((!hardKeyboard || (hardKeyboard && hardKeyboardHidden)) && sendFunctionKey(keyCode)) {
    196             return true;
    197           }
    198 
    199           // Support CTRL-a through CTRL-z
    200           if (key >= 0x61 && key <= 0x7A) {
    201             key -= 0x60;
    202           } else if (key >= 0x41 && key <= 0x5F) {
    203             key -= 0x40;
    204           } else if (key == 0x20) {
    205             key = 0x00;
    206           } else if (key == 0x3F) {
    207             key = 0x7F;
    208           }
    209         }
    210 
    211         // handle pressing f-keys
    212         // Doesn't work properly with asus keyboards... may never have worked. RM 09-Apr-2012
    213         /*
    214          * if ((hardKeyboard && !hardKeyboardHidden) && (curMetaState & KeyEvent.META_SHIFT_ON) != 0
    215          * && sendFunctionKey(keyCode)) { return true; }
    216          */
    217 
    218         if (key < 0x80) {
    219           transport.write(key);
    220         } else {
    221           // TODO write encoding routine that doesn't allocate each time
    222           transport.write(new String(Character.toChars(key)).getBytes(encoding));
    223         }
    224 
    225         return true;
    226       }
    227 
    228       if (keyCode == KeyEvent.KEYCODE_UNKNOWN && event.getAction() == KeyEvent.ACTION_MULTIPLE) {
    229         byte[] input = event.getCharacters().getBytes(encoding);
    230         transport.write(input);
    231         return true;
    232       }
    233 
    234       // try handling keymode shortcuts
    235       if (hardKeyboard && !hardKeyboardHidden && event.getRepeatCount() == 0) {
    236         if (PreferenceConstants.KEYMODE_RIGHT.equals(keymode)) {
    237           switch (keyCode) {
    238           case KeyEvent.KEYCODE_ALT_RIGHT:
    239             metaState |= META_SLASH;
    240             return true;
    241           case KeyEvent.KEYCODE_SHIFT_RIGHT:
    242             metaState |= META_TAB;
    243             return true;
    244           case KeyEvent.KEYCODE_SHIFT_LEFT:
    245             metaPress(META_SHIFT_ON);
    246             return true;
    247           case KeyEvent.KEYCODE_ALT_LEFT:
    248             metaPress(META_ALT_ON);
    249             return true;
    250           }
    251         } else if (PreferenceConstants.KEYMODE_LEFT.equals(keymode)) {
    252           switch (keyCode) {
    253           case KeyEvent.KEYCODE_ALT_LEFT:
    254             metaState |= META_SLASH;
    255             return true;
    256           case KeyEvent.KEYCODE_SHIFT_LEFT:
    257             metaState |= META_TAB;
    258             return true;
    259           case KeyEvent.KEYCODE_SHIFT_RIGHT:
    260             metaPress(META_SHIFT_ON);
    261             return true;
    262           case KeyEvent.KEYCODE_ALT_RIGHT:
    263             metaPress(META_ALT_ON);
    264             return true;
    265           }
    266         } else {
    267           switch (keyCode) {
    268           case KeyEvent.KEYCODE_ALT_LEFT:
    269           case KeyEvent.KEYCODE_ALT_RIGHT:
    270             metaPress(META_ALT_ON);
    271             return true;
    272           case KeyEvent.KEYCODE_SHIFT_LEFT:
    273           case KeyEvent.KEYCODE_SHIFT_RIGHT:
    274             metaPress(META_SHIFT_ON);
    275             return true;
    276           }
    277         }
    278       }
    279 
    280       // look for special chars
    281       switch (keyCode) {
    282       case KeyEvent.KEYCODE_CAMERA:
    283 
    284         // check to see which shortcut the camera button triggers
    285         String camera =
    286             manager.getStringParameter(PreferenceConstants.CAMERA,
    287                 PreferenceConstants.CAMERA_CTRLA_SPACE);
    288         if (PreferenceConstants.CAMERA_CTRLA_SPACE.equals(camera)) {
    289           transport.write(0x01);
    290           transport.write(' ');
    291         } else if (PreferenceConstants.CAMERA_CTRLA.equals(camera)) {
    292           transport.write(0x01);
    293         } else if (PreferenceConstants.CAMERA_ESC.equals(camera)) {
    294           ((vt320) buffer).keyTyped(vt320.KEY_ESCAPE, ' ', 0);
    295         } else if (PreferenceConstants.CAMERA_ESC_A.equals(camera)) {
    296           ((vt320) buffer).keyTyped(vt320.KEY_ESCAPE, ' ', 0);
    297           transport.write('a');
    298         }
    299 
    300         break;
    301 
    302       case KeyEvent.KEYCODE_DEL:
    303         ((vt320) buffer).keyPressed(vt320.KEY_BACK_SPACE, ' ', getStateForBuffer());
    304         metaState &= ~META_TRANSIENT;
    305         return true;
    306       case KeyEvent.KEYCODE_ENTER:
    307         ((vt320) buffer).keyTyped(vt320.KEY_ENTER, ' ', 0);
    308         metaState &= ~META_TRANSIENT;
    309         return true;
    310 
    311       case KeyEvent.KEYCODE_DPAD_LEFT:
    312         if (selectingForCopy) {
    313           selectionArea.decrementColumn();
    314           bridge.redraw();
    315         } else {
    316           ((vt320) buffer).keyPressed(vt320.KEY_LEFT, ' ', getStateForBuffer());
    317           metaState &= ~META_TRANSIENT;
    318           bridge.tryKeyVibrate();
    319         }
    320         return true;
    321 
    322       case KeyEvent.KEYCODE_DPAD_UP:
    323         if (selectingForCopy) {
    324           selectionArea.decrementRow();
    325           bridge.redraw();
    326         } else {
    327           ((vt320) buffer).keyPressed(vt320.KEY_UP, ' ', getStateForBuffer());
    328           metaState &= ~META_TRANSIENT;
    329           bridge.tryKeyVibrate();
    330         }
    331         return true;
    332 
    333       case KeyEvent.KEYCODE_DPAD_DOWN:
    334         if (selectingForCopy) {
    335           selectionArea.incrementRow();
    336           bridge.redraw();
    337         } else {
    338           ((vt320) buffer).keyPressed(vt320.KEY_DOWN, ' ', getStateForBuffer());
    339           metaState &= ~META_TRANSIENT;
    340           bridge.tryKeyVibrate();
    341         }
    342         return true;
    343 
    344       case KeyEvent.KEYCODE_DPAD_RIGHT:
    345         if (selectingForCopy) {
    346           selectionArea.incrementColumn();
    347           bridge.redraw();
    348         } else {
    349           ((vt320) buffer).keyPressed(vt320.KEY_RIGHT, ' ', getStateForBuffer());
    350           metaState &= ~META_TRANSIENT;
    351           bridge.tryKeyVibrate();
    352         }
    353         return true;
    354 
    355       case KeyEvent.KEYCODE_DPAD_CENTER:
    356         if (selectingForCopy) {
    357           if (selectionArea.isSelectingOrigin()) {
    358             selectionArea.finishSelectingOrigin();
    359           } else {
    360             if (clipboard != null) {
    361               // copy selected area to clipboard
    362               String copiedText = selectionArea.copyFrom(buffer);
    363 
    364               clipboard.setText(copiedText);
    365               // XXX STOPSHIP
    366               // manager.notifyUser(manager.getString(
    367               // R.string.console_copy_done,
    368               // copiedText.length()));
    369 
    370               selectingForCopy = false;
    371               selectionArea.reset();
    372             }
    373           }
    374         } else {
    375           if ((metaState & META_CTRL_ON) != 0) {
    376             ((vt320) buffer).keyTyped(vt320.KEY_ESCAPE, ' ', 0);
    377             metaState &= ~META_CTRL_ON;
    378           } else {
    379             metaState |= META_CTRL_ON;
    380           }
    381         }
    382 
    383         bridge.redraw();
    384 
    385         return true;
    386       }
    387 
    388     } catch (IOException e) {
    389       Log.e("Problem while trying to handle an onKey() event", e);
    390       try {
    391         bridge.getTransport().flush();
    392       } catch (IOException ioe) {
    393         Log.d("Our transport was closed, dispatching disconnect event");
    394         bridge.dispatchDisconnect(false);
    395       }
    396     } catch (NullPointerException npe) {
    397       Log.d("Input before connection established ignored.");
    398       return true;
    399     }
    400 
    401     return false;
    402   }
    403 
    404   /**
    405    * @param keyCode
    406    * @return successful
    407    */
    408   private boolean sendFunctionKey(int keyCode) {
    409     switch (keyCode) {
    410     case KeyEvent.KEYCODE_1:
    411       ((vt320) buffer).keyPressed(vt320.KEY_F1, ' ', 0);
    412       return true;
    413     case KeyEvent.KEYCODE_2:
    414       ((vt320) buffer).keyPressed(vt320.KEY_F2, ' ', 0);
    415       return true;
    416     case KeyEvent.KEYCODE_3:
    417       ((vt320) buffer).keyPressed(vt320.KEY_F3, ' ', 0);
    418       return true;
    419     case KeyEvent.KEYCODE_4:
    420       ((vt320) buffer).keyPressed(vt320.KEY_F4, ' ', 0);
    421       return true;
    422     case KeyEvent.KEYCODE_5:
    423       ((vt320) buffer).keyPressed(vt320.KEY_F5, ' ', 0);
    424       return true;
    425     case KeyEvent.KEYCODE_6:
    426       ((vt320) buffer).keyPressed(vt320.KEY_F6, ' ', 0);
    427       return true;
    428     case KeyEvent.KEYCODE_7:
    429       ((vt320) buffer).keyPressed(vt320.KEY_F7, ' ', 0);
    430       return true;
    431     case KeyEvent.KEYCODE_8:
    432       ((vt320) buffer).keyPressed(vt320.KEY_F8, ' ', 0);
    433       return true;
    434     case KeyEvent.KEYCODE_9:
    435       ((vt320) buffer).keyPressed(vt320.KEY_F9, ' ', 0);
    436       return true;
    437     case KeyEvent.KEYCODE_0:
    438       ((vt320) buffer).keyPressed(vt320.KEY_F10, ' ', 0);
    439       return true;
    440     default:
    441       return false;
    442     }
    443   }
    444 
    445   /**
    446    * Handle meta key presses where the key can be locked on.
    447    * <p>
    448    * 1st press: next key to have meta state<br />
    449    * 2nd press: meta state is locked on<br />
    450    * 3rd press: disable meta state
    451    *
    452    * @param code
    453    */
    454   private void metaPress(int code) {
    455     if ((metaState & (code << 1)) != 0) {
    456       metaState &= ~(code << 1);
    457     } else if ((metaState & code) != 0) {
    458       metaState &= ~code;
    459       metaState |= code << 1;
    460     } else {
    461       metaState |= code;
    462     }
    463     bridge.redraw();
    464   }
    465 
    466   public void setTerminalKeyMode(String keymode) {
    467     this.keymode = keymode;
    468   }
    469 
    470   private int getStateForBuffer() {
    471     int bufferState = 0;
    472 
    473     if ((metaState & META_CTRL_MASK) != 0) {
    474       bufferState |= vt320.KEY_CONTROL;
    475     }
    476     if ((metaState & META_SHIFT_MASK) != 0) {
    477       bufferState |= vt320.KEY_SHIFT;
    478     }
    479     if ((metaState & META_ALT_MASK) != 0) {
    480       bufferState |= vt320.KEY_ALT;
    481     }
    482 
    483     return bufferState;
    484   }
    485 
    486   public int getMetaState() {
    487     return metaState;
    488   }
    489 
    490   public void setClipboardManager(ClipboardManager clipboard) {
    491     this.clipboard = clipboard;
    492   }
    493 
    494   public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
    495     if (PreferenceConstants.KEYMODE.equals(key)) {
    496       updateKeymode();
    497     }
    498   }
    499 
    500   private void updateKeymode() {
    501     keymode =
    502         manager.getStringParameter(PreferenceConstants.KEYMODE, PreferenceConstants.KEYMODE_RIGHT);
    503   }
    504 
    505   public void setCharset(String encoding) {
    506     this.encoding = encoding;
    507   }
    508 }
    509