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