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 TerminalKeyListener(TerminalManager manager, TerminalBridge bridge, VDUBuffer buffer, String encoding)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 */ onKey(View v, int keyCode, KeyEvent event)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 */ sendFunctionKey(int keyCode)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 */ metaPress(int code)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 setTerminalKeyMode(String keymode)466 public void setTerminalKeyMode(String keymode) { 467 this.keymode = keymode; 468 } 469 getStateForBuffer()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 getMetaState()486 public int getMetaState() { 487 return metaState; 488 } 489 setClipboardManager(ClipboardManager clipboard)490 public void setClipboardManager(ClipboardManager clipboard) { 491 this.clipboard = clipboard; 492 } 493 onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key)494 public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { 495 if (PreferenceConstants.KEYMODE.equals(key)) { 496 updateKeymode(); 497 } 498 } 499 updateKeymode()500 private void updateKeymode() { 501 keymode = 502 manager.getStringParameter(PreferenceConstants.KEYMODE, PreferenceConstants.KEYMODE_RIGHT); 503 } 504 setCharset(String encoding)505 public void setCharset(String encoding) { 506 this.encoding = encoding; 507 } 508 } 509