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