2 * ConnectBot: simple, powerful, open-source SSH client for Android
3 * Copyright 2010 Kenny Root, Jeffrey Sharkey
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
9 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package org.connectbot.service;
19 import java.io.IOException;
21 import org.connectbot.TerminalView;
22 import org.connectbot.bean.SelectionArea;
23 import org.connectbot.util.PreferenceConstants;
25 import android.content.SharedPreferences;
26 import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
27 import android.content.res.Configuration;
28 import android.preference.PreferenceManager;
29 import android.text.ClipboardManager;
30 import android.util.Log;
31 import android.view.KeyCharacterMap;
32 import android.view.KeyEvent;
33 import android.view.View;
34 import android.view.View.OnKeyListener;
35 import de.mud.terminal.VDUBuffer;
36 import de.mud.terminal.vt320;
42 public class TerminalKeyListener implements OnKeyListener, OnSharedPreferenceChangeListener {
43 private static final String TAG = "ConnectBot.OnKeyListener";
45 public final static int META_CTRL_ON = 0x01;
46 public final static int META_CTRL_LOCK = 0x02;
47 public final static int META_ALT_ON = 0x04;
48 public final static int META_ALT_LOCK = 0x08;
49 public final static int META_SHIFT_ON = 0x10;
50 public final static int META_SHIFT_LOCK = 0x20;
51 public final static int META_SLASH = 0x40;
52 public final static int META_TAB = 0x80;
54 // The bit mask of momentary and lock states for each
55 public final static int META_CTRL_MASK = META_CTRL_ON | META_CTRL_LOCK;
56 public final static int META_ALT_MASK = META_ALT_ON | META_ALT_LOCK;
57 public final static int META_SHIFT_MASK = META_SHIFT_ON | META_SHIFT_LOCK;
59 // All the transient key codes
60 public final static int META_TRANSIENT = META_CTRL_ON | META_ALT_ON
63 private final TerminalManager manager;
64 private final TerminalBridge bridge;
65 private final VDUBuffer buffer;
67 protected KeyCharacterMap keymap = KeyCharacterMap.load(KeyCharacterMap.BUILT_IN_KEYBOARD);
69 private String keymode = null;
70 private boolean hardKeyboard = false;
72 private int metaState = 0;
74 private ClipboardManager clipboard = null;
75 private boolean selectingForCopy = false;
76 private final SelectionArea selectionArea;
78 private String encoding;
80 private final SharedPreferences prefs;
82 public TerminalKeyListener(TerminalManager manager,
83 TerminalBridge bridge,
86 this.manager = manager;
89 this.encoding = encoding;
91 selectionArea = new SelectionArea();
93 prefs = PreferenceManager.getDefaultSharedPreferences(manager);
94 prefs.registerOnSharedPreferenceChangeListener(this);
96 hardKeyboard = (manager.res.getConfiguration().keyboard
97 == Configuration.KEYBOARD_QWERTY);
103 * Handle onKey() events coming down from a {@link TerminalView} above us.
104 * Modify the keys to make more sense to a host then pass it to the transport.
106 public boolean onKey(View v, int keyCode, KeyEvent event) {
108 final boolean hardKeyboardHidden = manager.hardKeyboardHidden;
110 // Ignore all key-up events except for the special keys
111 if (event.getAction() == KeyEvent.ACTION_UP) {
112 // There's nothing here for virtual keyboard users.
113 if (!hardKeyboard || (hardKeyboard && hardKeyboardHidden))
116 // skip keys if we aren't connected yet or have been disconnected
117 if (bridge.isDisconnected() || bridge.transport == null)
120 if (PreferenceConstants.KEYMODE_RIGHT.equals(keymode)) {
121 if (keyCode == KeyEvent.KEYCODE_ALT_RIGHT
122 && (metaState & META_SLASH) != 0) {
123 metaState &= ~(META_SLASH | META_TRANSIENT);
124 bridge.transport.write('/');
126 } else if (keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT
127 && (metaState & META_TAB) != 0) {
128 metaState &= ~(META_TAB | META_TRANSIENT);
129 bridge.transport.write(0x09);
132 } else if (PreferenceConstants.KEYMODE_LEFT.equals(keymode)) {
133 if (keyCode == KeyEvent.KEYCODE_ALT_LEFT
134 && (metaState & META_SLASH) != 0) {
135 metaState &= ~(META_SLASH | META_TRANSIENT);
136 bridge.transport.write('/');
138 } else if (keyCode == KeyEvent.KEYCODE_SHIFT_LEFT
139 && (metaState & META_TAB) != 0) {
140 metaState &= ~(META_TAB | META_TRANSIENT);
141 bridge.transport.write(0x09);
149 // check for terminal resizing keys
150 if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
151 bridge.increaseFontSize();
153 } else if(keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
154 bridge.decreaseFontSize();
158 // skip keys if we aren't connected yet or have been disconnected
159 if (bridge.isDisconnected() || bridge.transport == null)
162 bridge.resetScrollPosition();
164 boolean printing = (keymap.isPrintingKey(keyCode) || keyCode == KeyEvent.KEYCODE_SPACE);
166 // otherwise pass through to existing session
169 int curMetaState = event.getMetaState();
171 metaState &= ~(META_SLASH | META_TAB);
173 if ((metaState & META_SHIFT_MASK) != 0) {
174 curMetaState |= KeyEvent.META_SHIFT_ON;
175 metaState &= ~META_SHIFT_ON;
179 if ((metaState & META_ALT_MASK) != 0) {
180 curMetaState |= KeyEvent.META_ALT_ON;
181 metaState &= ~META_ALT_ON;
185 int key = keymap.get(keyCode, curMetaState);
187 if ((metaState & META_CTRL_MASK) != 0) {
188 metaState &= ~META_CTRL_ON;
191 if ((!hardKeyboard || (hardKeyboard && hardKeyboardHidden))
192 && sendFunctionKey(keyCode))
195 // Support CTRL-a through CTRL-z
196 if (key >= 0x61 && key <= 0x7A)
198 // Support CTRL-A through CTRL-_
199 else if (key >= 0x41 && key <= 0x5F)
201 else if (key == 0x20)
203 else if (key == 0x3F)
207 // handle pressing f-keys
208 if ((hardKeyboard && !hardKeyboardHidden)
209 && (curMetaState & KeyEvent.META_SHIFT_ON) != 0
210 && sendFunctionKey(keyCode))
214 bridge.transport.write(key);
216 // TODO write encoding routine that doesn't allocate each time
217 bridge.transport.write(new String(Character.toChars(key))
218 .getBytes(encoding));
223 if (keyCode == KeyEvent.KEYCODE_UNKNOWN &&
224 event.getAction() == KeyEvent.ACTION_MULTIPLE) {
225 byte[] input = event.getCharacters().getBytes(encoding);
226 bridge.transport.write(input);
230 // try handling keymode shortcuts
231 if (hardKeyboard && !hardKeyboardHidden &&
232 event.getRepeatCount() == 0) {
233 if (PreferenceConstants.KEYMODE_RIGHT.equals(keymode)) {
235 case KeyEvent.KEYCODE_ALT_RIGHT:
236 metaState |= META_SLASH;
238 case KeyEvent.KEYCODE_SHIFT_RIGHT:
239 metaState |= META_TAB;
241 case KeyEvent.KEYCODE_SHIFT_LEFT:
242 metaPress(META_SHIFT_ON);
244 case KeyEvent.KEYCODE_ALT_LEFT:
245 metaPress(META_ALT_ON);
248 } else if (PreferenceConstants.KEYMODE_LEFT.equals(keymode)) {
250 case KeyEvent.KEYCODE_ALT_LEFT:
251 metaState |= META_SLASH;
253 case KeyEvent.KEYCODE_SHIFT_LEFT:
254 metaState |= META_TAB;
256 case KeyEvent.KEYCODE_SHIFT_RIGHT:
257 metaPress(META_SHIFT_ON);
259 case KeyEvent.KEYCODE_ALT_RIGHT:
260 metaPress(META_ALT_ON);
265 case KeyEvent.KEYCODE_ALT_LEFT:
266 case KeyEvent.KEYCODE_ALT_RIGHT:
267 metaPress(META_ALT_ON);
269 case KeyEvent.KEYCODE_SHIFT_LEFT:
270 case KeyEvent.KEYCODE_SHIFT_RIGHT:
271 metaPress(META_SHIFT_ON);
277 // look for special chars
279 case KeyEvent.KEYCODE_CAMERA:
281 // check to see which shortcut the camera button triggers
282 String camera = manager.prefs.getString(
283 PreferenceConstants.CAMERA,
284 PreferenceConstants.CAMERA_CTRLA_SPACE);
285 if(PreferenceConstants.CAMERA_CTRLA_SPACE.equals(camera)) {
286 bridge.transport.write(0x01);
287 bridge.transport.write(' ');
288 } else if(PreferenceConstants.CAMERA_CTRLA.equals(camera)) {
289 bridge.transport.write(0x01);
290 } else if(PreferenceConstants.CAMERA_ESC.equals(camera)) {
291 ((vt320)buffer).keyTyped(vt320.KEY_ESCAPE, ' ', 0);
292 } else if(PreferenceConstants.CAMERA_ESC_A.equals(camera)) {
293 ((vt320)buffer).keyTyped(vt320.KEY_ESCAPE, ' ', 0);
294 bridge.transport.write('a');
299 case KeyEvent.KEYCODE_DEL:
300 ((vt320) buffer).keyPressed(vt320.KEY_BACK_SPACE, ' ',
301 getStateForBuffer());
302 metaState &= ~META_TRANSIENT;
304 case KeyEvent.KEYCODE_ENTER:
305 ((vt320)buffer).keyTyped(vt320.KEY_ENTER, ' ', 0);
306 metaState &= ~META_TRANSIENT;
309 case KeyEvent.KEYCODE_DPAD_LEFT:
310 if (selectingForCopy) {
311 selectionArea.decrementColumn();
314 ((vt320) buffer).keyPressed(vt320.KEY_LEFT, ' ',
315 getStateForBuffer());
316 metaState &= ~META_TRANSIENT;
317 bridge.tryKeyVibrate();
321 case KeyEvent.KEYCODE_DPAD_UP:
322 if (selectingForCopy) {
323 selectionArea.decrementRow();
326 ((vt320) buffer).keyPressed(vt320.KEY_UP, ' ',
327 getStateForBuffer());
328 metaState &= ~META_TRANSIENT;
329 bridge.tryKeyVibrate();
333 case KeyEvent.KEYCODE_DPAD_DOWN:
334 if (selectingForCopy) {
335 selectionArea.incrementRow();
338 ((vt320) buffer).keyPressed(vt320.KEY_DOWN, ' ',
339 getStateForBuffer());
340 metaState &= ~META_TRANSIENT;
341 bridge.tryKeyVibrate();
345 case KeyEvent.KEYCODE_DPAD_RIGHT:
346 if (selectingForCopy) {
347 selectionArea.incrementColumn();
350 ((vt320) buffer).keyPressed(vt320.KEY_RIGHT, ' ',
351 getStateForBuffer());
352 metaState &= ~META_TRANSIENT;
353 bridge.tryKeyVibrate();
357 case KeyEvent.KEYCODE_DPAD_CENTER:
358 if (selectingForCopy) {
359 if (selectionArea.isSelectingOrigin())
360 selectionArea.finishSelectingOrigin();
362 if (clipboard != null) {
363 // copy selected area to clipboard
364 String copiedText = selectionArea.copyFrom(buffer);
366 clipboard.setText(copiedText);
368 // manager.notifyUser(manager.getString(
369 // R.string.console_copy_done,
370 // copiedText.length()));
372 selectingForCopy = false;
373 selectionArea.reset();
377 if ((metaState & META_CTRL_ON) != 0) {
379 metaState &= ~META_CTRL_ON;
381 metaPress(META_CTRL_ON);
389 } catch (IOException e) {
390 Log.e(TAG, "Problem while trying to handle an onKey() event", e);
392 bridge.transport.flush();
393 } catch (IOException ioe) {
394 Log.d(TAG, "Our transport was closed, dispatching disconnect event");
395 bridge.dispatchDisconnect(false);
397 } catch (NullPointerException npe) {
398 Log.d(TAG, "Input before connection established ignored.");
405 public void sendEscape() {
406 ((vt320)buffer).keyTyped(vt320.KEY_ESCAPE, ' ', 0);
413 private boolean sendFunctionKey(int keyCode) {
415 case KeyEvent.KEYCODE_1:
416 ((vt320) buffer).keyPressed(vt320.KEY_F1, ' ', 0);
418 case KeyEvent.KEYCODE_2:
419 ((vt320) buffer).keyPressed(vt320.KEY_F2, ' ', 0);
421 case KeyEvent.KEYCODE_3:
422 ((vt320) buffer).keyPressed(vt320.KEY_F3, ' ', 0);
424 case KeyEvent.KEYCODE_4:
425 ((vt320) buffer).keyPressed(vt320.KEY_F4, ' ', 0);
427 case KeyEvent.KEYCODE_5:
428 ((vt320) buffer).keyPressed(vt320.KEY_F5, ' ', 0);
430 case KeyEvent.KEYCODE_6:
431 ((vt320) buffer).keyPressed(vt320.KEY_F6, ' ', 0);
433 case KeyEvent.KEYCODE_7:
434 ((vt320) buffer).keyPressed(vt320.KEY_F7, ' ', 0);
436 case KeyEvent.KEYCODE_8:
437 ((vt320) buffer).keyPressed(vt320.KEY_F8, ' ', 0);
439 case KeyEvent.KEYCODE_9:
440 ((vt320) buffer).keyPressed(vt320.KEY_F9, ' ', 0);
442 case KeyEvent.KEYCODE_0:
443 ((vt320) buffer).keyPressed(vt320.KEY_F10, ' ', 0);
451 * Handle meta key presses where the key can be locked on.
453 * 1st press: next key to have meta state<br />
454 * 2nd press: meta state is locked on<br />
455 * 3rd press: disable meta state
459 public void metaPress(int code) {
460 if ((metaState & (code << 1)) != 0) {
461 metaState &= ~(code << 1);
462 } else if ((metaState & code) != 0) {
464 metaState |= code << 1;
470 public void setTerminalKeyMode(String keymode) {
471 this.keymode = keymode;
474 private int getStateForBuffer() {
477 if ((metaState & META_CTRL_MASK) != 0)
478 bufferState |= vt320.KEY_CONTROL;
479 if ((metaState & META_SHIFT_MASK) != 0)
480 bufferState |= vt320.KEY_SHIFT;
481 if ((metaState & META_ALT_MASK) != 0)
482 bufferState |= vt320.KEY_ALT;
487 public int getMetaState() {
491 public void setClipboardManager(ClipboardManager clipboard) {
492 this.clipboard = clipboard;
495 public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
497 if (PreferenceConstants.KEYMODE.equals(key)) {
502 private void updateKeymode() {
503 keymode = prefs.getString(PreferenceConstants.KEYMODE, PreferenceConstants.KEYMODE_RIGHT);
506 public void setCharset(String encoding) {
507 this.encoding = encoding;