• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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