• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * ConnectBot: simple, powerful, open-source SSH client for Android
3  * Copyright 2007 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 
18 /**
19  * @author modified by raaar
20  *
21  */
22 
23 package org.connectbot;
24 
25 import android.app.Activity;
26 import android.app.AlertDialog;
27 import android.content.ComponentName;
28 import android.content.Context;
29 import android.content.DialogInterface;
30 import android.content.Intent;
31 import android.content.ServiceConnection;
32 import android.content.SharedPreferences;
33 import android.content.pm.ActivityInfo;
34 import android.content.res.Configuration;
35 import android.media.AudioManager;
36 import android.net.Uri;
37 import android.os.Bundle;
38 import android.os.Handler;
39 import android.os.IBinder;
40 import android.os.Message;
41 import android.os.PowerManager;
42 import android.preference.PreferenceManager;
43 import android.text.ClipboardManager;
44 import android.view.ContextMenu;
45 import android.view.ContextMenu.ContextMenuInfo;
46 import android.view.GestureDetector;
47 import android.view.LayoutInflater;
48 import android.view.Menu;
49 import android.view.MenuItem;
50 import android.view.MotionEvent;
51 import android.view.View;
52 import android.view.View.OnClickListener;
53 import android.view.View.OnTouchListener;
54 import android.view.ViewConfiguration;
55 import android.view.WindowManager;
56 import android.view.animation.Animation;
57 import android.view.animation.AnimationUtils;
58 import android.view.inputmethod.InputMethodManager;
59 import android.widget.Button;
60 import android.widget.EditText;
61 import android.widget.ImageView;
62 import android.widget.RelativeLayout;
63 import android.widget.TextView;
64 import android.widget.Toast;
65 import android.widget.ViewFlipper;
66 
67 import com.googlecode.android_scripting.Constants;
68 import com.googlecode.android_scripting.Log;
69 import com.googlecode.android_scripting.R;
70 import com.googlecode.android_scripting.ScriptProcess;
71 import com.googlecode.android_scripting.activity.Preferences;
72 import com.googlecode.android_scripting.activity.ScriptingLayerService;
73 
74 import de.mud.terminal.VDUBuffer;
75 import de.mud.terminal.vt320;
76 
77 import org.connectbot.service.PromptHelper;
78 import org.connectbot.service.TerminalBridge;
79 import org.connectbot.service.TerminalManager;
80 import org.connectbot.util.PreferenceConstants;
81 import org.connectbot.util.SelectionArea;
82 
83 public class ConsoleActivity extends Activity {
84 
85   protected static final int REQUEST_EDIT = 1;
86 
87   private static final int CLICK_TIME = 250;
88   private static final float MAX_CLICK_DISTANCE = 25f;
89   private static final int KEYBOARD_DISPLAY_TIME = 1250;
90 
91   // Direction to shift the ViewFlipper
92   private static final int SHIFT_LEFT = 0;
93   private static final int SHIFT_RIGHT = 1;
94 
95   protected ViewFlipper flip = null;
96   protected TerminalManager manager = null;
97   protected ScriptingLayerService mService = null;
98   protected LayoutInflater inflater = null;
99 
100   private SharedPreferences prefs = null;
101 
102   private PowerManager.WakeLock wakelock = null;
103 
104   protected Integer processID;
105 
106   protected ClipboardManager clipboard;
107 
108   private RelativeLayout booleanPromptGroup;
109   private TextView booleanPrompt;
110   private Button booleanYes, booleanNo;
111 
112   private Animation slide_left_in, slide_left_out, slide_right_in, slide_right_out,
113       fade_stay_hidden, fade_out_delayed;
114 
115   private Animation keyboard_fade_in, keyboard_fade_out;
116   private ImageView keyboardButton;
117   private float lastX, lastY;
118 
119   private int mTouchSlopSquare;
120 
121   private InputMethodManager inputManager;
122 
123   protected TerminalBridge copySource = null;
124   private int lastTouchRow, lastTouchCol;
125 
126   private boolean forcedOrientation;
127 
128   private Handler handler = new Handler();
129 
130   private static enum MenuId {
131     EDIT, PREFS, EMAIL, RESIZE, COPY, PASTE;
getId()132     public int getId() {
133       return ordinal() + Menu.FIRST;
134     }
135   }
136 
137   private final ServiceConnection mConnection = new ServiceConnection() {
138     @Override
139     public void onServiceConnected(ComponentName name, IBinder service) {
140       mService = ((ScriptingLayerService.LocalBinder) service).getService();
141       manager = mService.getTerminalManager();
142       // let manager know about our event handling services
143       manager.setDisconnectHandler(disconnectHandler);
144 
145       Log.d(String.format("Connected to TerminalManager and found bridges.size=%d", manager
146           .getBridgeList().size()));
147 
148       manager.setResizeAllowed(true);
149 
150       // clear out any existing bridges and record requested index
151       flip.removeAllViews();
152 
153       int requestedIndex = 0;
154 
155       TerminalBridge requestedBridge = manager.getConnectedBridge(processID);
156 
157       // If we didn't find the requested connection, try opening it
158       if (processID != null && requestedBridge == null) {
159         try {
160           Log.d(String.format(
161               "We couldnt find an existing bridge with id = %d, so creating one now", processID));
162           requestedBridge = manager.openConnection(processID);
163         } catch (Exception e) {
164           Log.e("Problem while trying to create new requested bridge", e);
165         }
166       }
167 
168       // create views for all bridges on this service
169       for (TerminalBridge bridge : manager.getBridgeList()) {
170 
171         final int currentIndex = addNewTerminalView(bridge);
172 
173         // check to see if this bridge was requested
174         if (bridge == requestedBridge) {
175           requestedIndex = currentIndex;
176         }
177       }
178 
179       setDisplayedTerminal(requestedIndex);
180     }
181 
182     @Override
183     public void onServiceDisconnected(ComponentName name) {
184       manager = null;
185       mService = null;
186     }
187   };
188 
189   protected Handler promptHandler = new Handler() {
190     @Override
191     public void handleMessage(Message msg) {
192       // someone below us requested to display a prompt
193       updatePromptVisible();
194     }
195   };
196 
197   protected Handler disconnectHandler = new Handler() {
198     @Override
199     public void handleMessage(Message msg) {
200       Log.d("Someone sending HANDLE_DISCONNECT to parentHandler");
201       TerminalBridge bridge = (TerminalBridge) msg.obj;
202       closeBridge(bridge);
203     }
204   };
205 
206   /**
207    * @param bridge
208    */
closeBridge(final TerminalBridge bridge)209   private void closeBridge(final TerminalBridge bridge) {
210     synchronized (flip) {
211       final int flipIndex = getFlipIndex(bridge);
212 
213       if (flipIndex >= 0) {
214         if (flip.getDisplayedChild() == flipIndex) {
215           shiftCurrentTerminal(SHIFT_LEFT);
216         }
217         flip.removeViewAt(flipIndex);
218 
219         /*
220          * TODO Remove this workaround when ViewFlipper is fixed to listen to view removals. Android
221          * Issue 1784
222          */
223         final int numChildren = flip.getChildCount();
224         if (flip.getDisplayedChild() >= numChildren && numChildren > 0) {
225           flip.setDisplayedChild(numChildren - 1);
226         }
227       }
228 
229       // If we just closed the last bridge, go back to the previous activity.
230       if (flip.getChildCount() == 0) {
231         finish();
232       }
233     }
234   }
235 
findCurrentView(int id)236   protected View findCurrentView(int id) {
237     View view = flip.getCurrentView();
238     if (view == null) {
239       return null;
240     }
241     return view.findViewById(id);
242   }
243 
getCurrentPromptHelper()244   protected PromptHelper getCurrentPromptHelper() {
245     View view = findCurrentView(R.id.console_flip);
246     if (!(view instanceof TerminalView)) {
247       return null;
248     }
249     return ((TerminalView) view).bridge.getPromptHelper();
250   }
251 
hideAllPrompts()252   protected void hideAllPrompts() {
253     booleanPromptGroup.setVisibility(View.GONE);
254   }
255 
256   @Override
onCreate(Bundle icicle)257   public void onCreate(Bundle icicle) {
258     super.onCreate(icicle);
259 
260     this.setContentView(R.layout.act_console);
261 
262     clipboard = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
263     prefs = PreferenceManager.getDefaultSharedPreferences(this);
264 
265     // hide status bar if requested by user
266     if (prefs.getBoolean(PreferenceConstants.FULLSCREEN, false)) {
267       getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
268           WindowManager.LayoutParams.FLAG_FULLSCREEN);
269     }
270 
271     // TODO find proper way to disable volume key beep if it exists.
272     setVolumeControlStream(AudioManager.STREAM_MUSIC);
273 
274     PowerManager manager = (PowerManager) getSystemService(Context.POWER_SERVICE);
275     wakelock = manager.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK, getPackageName());
276 
277     // handle requested console from incoming intent
278     int id = getIntent().getIntExtra(Constants.EXTRA_PROXY_PORT, -1);
279 
280     if (id > 0) {
281       processID = id;
282     }
283 
284     inflater = LayoutInflater.from(this);
285 
286     flip = (ViewFlipper) findViewById(R.id.console_flip);
287     booleanPromptGroup = (RelativeLayout) findViewById(R.id.console_boolean_group);
288     booleanPrompt = (TextView) findViewById(R.id.console_prompt);
289 
290     booleanYes = (Button) findViewById(R.id.console_prompt_yes);
291     booleanYes.setOnClickListener(new OnClickListener() {
292       public void onClick(View v) {
293         PromptHelper helper = getCurrentPromptHelper();
294         if (helper == null) {
295           return;
296         }
297         helper.setResponse(Boolean.TRUE);
298         updatePromptVisible();
299       }
300     });
301 
302     booleanNo = (Button) findViewById(R.id.console_prompt_no);
303     booleanNo.setOnClickListener(new OnClickListener() {
304       public void onClick(View v) {
305         PromptHelper helper = getCurrentPromptHelper();
306         if (helper == null) {
307           return;
308         }
309         helper.setResponse(Boolean.FALSE);
310         updatePromptVisible();
311       }
312     });
313 
314     // preload animations for terminal switching
315     slide_left_in = AnimationUtils.loadAnimation(this, R.anim.slide_left_in);
316     slide_left_out = AnimationUtils.loadAnimation(this, R.anim.slide_left_out);
317     slide_right_in = AnimationUtils.loadAnimation(this, R.anim.slide_right_in);
318     slide_right_out = AnimationUtils.loadAnimation(this, R.anim.slide_right_out);
319 
320     fade_out_delayed = AnimationUtils.loadAnimation(this, R.anim.fade_out_delayed);
321     fade_stay_hidden = AnimationUtils.loadAnimation(this, R.anim.fade_stay_hidden);
322 
323     // Preload animation for keyboard button
324     keyboard_fade_in = AnimationUtils.loadAnimation(this, R.anim.keyboard_fade_in);
325     keyboard_fade_out = AnimationUtils.loadAnimation(this, R.anim.keyboard_fade_out);
326 
327     inputManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
328     keyboardButton = (ImageView) findViewById(R.id.keyboard_button);
329     keyboardButton.setOnClickListener(new OnClickListener() {
330       public void onClick(View view) {
331         View flip = findCurrentView(R.id.console_flip);
332         if (flip == null) {
333           return;
334         }
335 
336         inputManager.showSoftInput(flip, InputMethodManager.SHOW_FORCED);
337         keyboardButton.setVisibility(View.GONE);
338       }
339     });
340     if (prefs.getBoolean(PreferenceConstants.HIDE_KEYBOARD, false)) {
341       // Force hidden keyboard.
342       getWindow().setSoftInputMode(
343           WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN
344               | WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
345     }
346     final ViewConfiguration configuration = ViewConfiguration.get(this);
347     int touchSlop = configuration.getScaledTouchSlop();
348     mTouchSlopSquare = touchSlop * touchSlop;
349 
350     // detect fling gestures to switch between terminals
351     final GestureDetector detect =
352         new GestureDetector(new GestureDetector.SimpleOnGestureListener() {
353           private float totalY = 0;
354 
355           @Override
356           public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
357 
358             final float distx = e2.getRawX() - e1.getRawX();
359             final float disty = e2.getRawY() - e1.getRawY();
360             final int goalwidth = flip.getWidth() / 2;
361 
362             // need to slide across half of display to trigger console change
363             // make sure user kept a steady hand horizontally
364             if (Math.abs(disty) < (flip.getHeight() / 4)) {
365               if (distx > goalwidth) {
366                 shiftCurrentTerminal(SHIFT_RIGHT);
367                 return true;
368               }
369 
370               if (distx < -goalwidth) {
371                 shiftCurrentTerminal(SHIFT_LEFT);
372                 return true;
373               }
374 
375             }
376 
377             return false;
378           }
379 
380           @Override
381           public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
382 
383             // if copying, then ignore
384             if (copySource != null && copySource.isSelectingForCopy()) {
385               return false;
386             }
387 
388             if (e1 == null || e2 == null) {
389               return false;
390             }
391 
392             // if releasing then reset total scroll
393             if (e2.getAction() == MotionEvent.ACTION_UP) {
394               totalY = 0;
395             }
396 
397             // activate consider if within x tolerance
398             if (Math.abs(e1.getX() - e2.getX()) < ViewConfiguration.getTouchSlop() * 4) {
399 
400               View flip = findCurrentView(R.id.console_flip);
401               if (flip == null) {
402                 return false;
403               }
404               TerminalView terminal = (TerminalView) flip;
405 
406               // estimate how many rows we have scrolled through
407               // accumulate distance that doesn't trigger immediate scroll
408               totalY += distanceY;
409               final int moved = (int) (totalY / terminal.bridge.charHeight);
410 
411               VDUBuffer buffer = terminal.bridge.getVDUBuffer();
412 
413               // consume as scrollback only if towards right half of screen
414               if (e2.getX() > flip.getWidth() / 2) {
415                 if (moved != 0) {
416                   int base = buffer.getWindowBase();
417                   buffer.setWindowBase(base + moved);
418                   totalY = 0;
419                   return true;
420                 }
421               } else {
422                 // otherwise consume as pgup/pgdown for every 5 lines
423                 if (moved > 5) {
424                   ((vt320) buffer).keyPressed(vt320.KEY_PAGE_DOWN, ' ', 0);
425                   terminal.bridge.tryKeyVibrate();
426                   totalY = 0;
427                   return true;
428                 } else if (moved < -5) {
429                   ((vt320) buffer).keyPressed(vt320.KEY_PAGE_UP, ' ', 0);
430                   terminal.bridge.tryKeyVibrate();
431                   totalY = 0;
432                   return true;
433                 }
434 
435               }
436 
437             }
438 
439             return false;
440           }
441 
442         });
443 
444     flip.setOnCreateContextMenuListener(this);
445 
446     flip.setOnTouchListener(new OnTouchListener() {
447 
448       public boolean onTouch(View v, MotionEvent event) {
449 
450         // when copying, highlight the area
451         if (copySource != null && copySource.isSelectingForCopy()) {
452           int row = (int) Math.floor(event.getY() / copySource.charHeight);
453           int col = (int) Math.floor(event.getX() / copySource.charWidth);
454 
455           SelectionArea area = copySource.getSelectionArea();
456 
457           switch (event.getAction()) {
458           case MotionEvent.ACTION_DOWN:
459             // recording starting area
460             if (area.isSelectingOrigin()) {
461               area.setRow(row);
462               area.setColumn(col);
463               lastTouchRow = row;
464               lastTouchCol = col;
465               copySource.redraw();
466             }
467             return true;
468           case MotionEvent.ACTION_MOVE:
469             /*
470              * ignore when user hasn't moved since last time so we can fine-tune with directional
471              * pad
472              */
473             if (row == lastTouchRow && col == lastTouchCol) {
474               return true;
475             }
476             // if the user moves, start the selection for other corner
477             area.finishSelectingOrigin();
478 
479             // update selected area
480             area.setRow(row);
481             area.setColumn(col);
482             lastTouchRow = row;
483             lastTouchCol = col;
484             copySource.redraw();
485             return true;
486           case MotionEvent.ACTION_UP:
487             /*
488              * If they didn't move their finger, maybe they meant to select the rest of the text
489              * with the directional pad.
490              */
491             if (area.getLeft() == area.getRight() && area.getTop() == area.getBottom()) {
492               return true;
493             }
494 
495             // copy selected area to clipboard
496             String copiedText = area.copyFrom(copySource.getVDUBuffer());
497 
498             clipboard.setText(copiedText);
499             Toast.makeText(ConsoleActivity.this,
500                 getString(R.string.terminal_copy_done, copiedText.length()), Toast.LENGTH_LONG)
501                 .show();
502             // fall through to clear state
503 
504           case MotionEvent.ACTION_CANCEL:
505             // make sure we clear any highlighted area
506             area.reset();
507             copySource.setSelectingForCopy(false);
508             copySource.redraw();
509             return true;
510           }
511         }
512 
513         Configuration config = getResources().getConfiguration();
514 
515         if (event.getAction() == MotionEvent.ACTION_DOWN) {
516           lastX = event.getX();
517           lastY = event.getY();
518         } else if (event.getAction() == MotionEvent.ACTION_MOVE) {
519           final int deltaX = (int) (lastX - event.getX());
520           final int deltaY = (int) (lastY - event.getY());
521           int distance = (deltaX * deltaX) + (deltaY * deltaY);
522           if (distance > mTouchSlopSquare) {
523             // If currently scheduled long press event is not canceled here,
524             // GestureDetector.onScroll is executed, which takes a while, and by the time we are
525             // back in the view's dispatchTouchEvent
526             // mPendingCheckForLongPress is already executed
527             flip.cancelLongPress();
528           }
529         } else if (event.getAction() == MotionEvent.ACTION_UP) {
530           // Same as above, except now GestureDetector.onFling is called.
531           flip.cancelLongPress();
532           if (config.hardKeyboardHidden != Configuration.KEYBOARDHIDDEN_NO
533               && keyboardButton.getVisibility() == View.GONE
534               && event.getEventTime() - event.getDownTime() < CLICK_TIME
535               && Math.abs(event.getX() - lastX) < MAX_CLICK_DISTANCE
536               && Math.abs(event.getY() - lastY) < MAX_CLICK_DISTANCE) {
537             keyboardButton.startAnimation(keyboard_fade_in);
538             keyboardButton.setVisibility(View.VISIBLE);
539 
540             handler.postDelayed(new Runnable() {
541               public void run() {
542                 if (keyboardButton.getVisibility() == View.GONE) {
543                   return;
544                 }
545 
546                 keyboardButton.startAnimation(keyboard_fade_out);
547                 keyboardButton.setVisibility(View.GONE);
548               }
549             }, KEYBOARD_DISPLAY_TIME);
550 
551             return false;
552           }
553         }
554         // pass any touch events back to detector
555         return detect.onTouchEvent(event);
556       }
557 
558     });
559 
560   }
561 
configureOrientation()562   private void configureOrientation() {
563     String rotateDefault;
564     if (getResources().getConfiguration().keyboard == Configuration.KEYBOARD_NOKEYS) {
565       rotateDefault = PreferenceConstants.ROTATION_PORTRAIT;
566     } else {
567       rotateDefault = PreferenceConstants.ROTATION_LANDSCAPE;
568     }
569 
570     String rotate = prefs.getString(PreferenceConstants.ROTATION, rotateDefault);
571     if (PreferenceConstants.ROTATION_DEFAULT.equals(rotate)) {
572       rotate = rotateDefault;
573     }
574 
575     // request a forced orientation if requested by user
576     if (PreferenceConstants.ROTATION_LANDSCAPE.equals(rotate)) {
577       setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
578       forcedOrientation = true;
579     } else if (PreferenceConstants.ROTATION_PORTRAIT.equals(rotate)) {
580       setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
581       forcedOrientation = true;
582     } else {
583       setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
584       forcedOrientation = false;
585     }
586   }
587 
588   @Override
onCreateOptionsMenu(Menu menu)589   public boolean onCreateOptionsMenu(Menu menu) {
590     super.onCreateOptionsMenu(menu);
591     getMenuInflater().inflate(R.menu.terminal, menu);
592     menu.setQwertyMode(true);
593     return true;
594   }
595 
596   @Override
onPrepareOptionsMenu(Menu menu)597   public boolean onPrepareOptionsMenu(Menu menu) {
598     super.onPrepareOptionsMenu(menu);
599     setVolumeControlStream(AudioManager.STREAM_NOTIFICATION);
600     TerminalBridge bridge = ((TerminalView) findCurrentView(R.id.console_flip)).bridge;
601     boolean sessionOpen = bridge.isSessionOpen();
602     menu.findItem(R.id.terminal_menu_resize).setEnabled(sessionOpen);
603     if (bridge.getProcess() instanceof ScriptProcess) {
604       menu.findItem(R.id.terminal_menu_exit_and_edit).setEnabled(true);
605     }
606     bridge.onPrepareOptionsMenu(menu);
607     return true;
608   }
609 
610   @Override
onOptionsItemSelected(MenuItem item)611   public boolean onOptionsItemSelected(MenuItem item) {
612     if (item.getItemId() == R.id.terminal_menu_resize) {
613       doResize();
614     } else if (item.getItemId() == R.id.terminal_menu_preferences) {
615       doPreferences();
616     } else if (item.getItemId() == R.id.terminal_menu_send_email) {
617       doEmailTranscript();
618     } else if (item.getItemId() == R.id.terminal_menu_exit_and_edit) {
619       TerminalView terminalView = (TerminalView) findCurrentView(R.id.console_flip);
620       TerminalBridge bridge = terminalView.bridge;
621       if (manager != null) {
622         manager.closeConnection(bridge, true);
623       } else {
624         Intent intent = new Intent(this, ScriptingLayerService.class);
625         intent.setAction(Constants.ACTION_KILL_PROCESS);
626         intent.putExtra(Constants.EXTRA_PROXY_PORT, bridge.getId());
627         startService(intent);
628         Message.obtain(disconnectHandler, -1, bridge).sendToTarget();
629       }
630       Intent intent = new Intent(Constants.ACTION_EDIT_SCRIPT);
631       ScriptProcess process = (ScriptProcess) bridge.getProcess();
632       intent.putExtra(Constants.EXTRA_SCRIPT_PATH, process.getPath());
633       startActivity(intent);
634       finish();
635     }
636     return super.onOptionsItemSelected(item);
637   }
638 
639   @Override
onOptionsMenuClosed(Menu menu)640   public void onOptionsMenuClosed(Menu menu) {
641     super.onOptionsMenuClosed(menu);
642     setVolumeControlStream(AudioManager.STREAM_MUSIC);
643   }
644 
doResize()645   private void doResize() {
646     closeOptionsMenu();
647     final TerminalView terminalView = (TerminalView) findCurrentView(R.id.console_flip);
648     final View resizeView = inflater.inflate(R.layout.dia_resize, null, false);
649     new AlertDialog.Builder(ConsoleActivity.this).setView(resizeView)
650         .setPositiveButton(R.string.button_resize, new DialogInterface.OnClickListener() {
651           public void onClick(DialogInterface dialog, int which) {
652             int width, height;
653             try {
654               width =
655                   Integer.parseInt(((EditText) resizeView.findViewById(R.id.width)).getText()
656                       .toString());
657               height =
658                   Integer.parseInt(((EditText) resizeView.findViewById(R.id.height)).getText()
659                       .toString());
660             } catch (NumberFormatException nfe) {
661               return;
662             }
663             terminalView.forceSize(width, height);
664           }
665         }).setNegativeButton(android.R.string.cancel, null).create().show();
666   }
667 
doPreferences()668   private void doPreferences() {
669     startActivity(new Intent(this, Preferences.class));
670   }
671 
doEmailTranscript()672   private void doEmailTranscript() {
673     // Don't really want to supply an address, but currently it's required,
674     // otherwise we get an exception.
675     TerminalView terminalView = (TerminalView) findCurrentView(R.id.console_flip);
676     TerminalBridge bridge = terminalView.bridge;
677     // TODO(raaar): Replace with process log.
678     VDUBuffer buffer = bridge.getVDUBuffer();
679     int height = buffer.getRows();
680     int width = buffer.getColumns();
681     StringBuilder string = new StringBuilder();
682     for (int i = 0; i < height; i++) {
683       for (int j = 0; j < width; j++) {
684         string.append(buffer.getChar(j, i));
685       }
686     }
687     String addr = "user@example.com";
688     Intent intent = new Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:" + addr));
689     intent.putExtra("body", string.toString().trim());
690     startActivity(intent);
691   }
692 
693   @Override
onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo)694   public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) {
695     TerminalBridge bridge = ((TerminalView) findCurrentView(R.id.console_flip)).bridge;
696     boolean sessionOpen = bridge.isSessionOpen();
697     menu.add(Menu.NONE, MenuId.COPY.getId(), Menu.NONE, R.string.terminal_menu_copy);
698     if (clipboard.hasText() && sessionOpen) {
699       menu.add(Menu.NONE, MenuId.PASTE.getId(), Menu.NONE, R.string.terminal_menu_paste);
700     }
701     bridge.onCreateContextMenu(menu, view, menuInfo);
702   }
703 
704   @Override
onContextItemSelected(MenuItem item)705   public boolean onContextItemSelected(MenuItem item) {
706     int itemId = item.getItemId();
707     if (itemId == MenuId.COPY.getId()) {
708       TerminalView terminalView = (TerminalView) findCurrentView(R.id.console_flip);
709       copySource = terminalView.bridge;
710       SelectionArea area = copySource.getSelectionArea();
711       area.reset();
712       area.setBounds(copySource.getVDUBuffer().getColumns(), copySource.getVDUBuffer().getRows());
713       copySource.setSelectingForCopy(true);
714       // Make sure we show the initial selection
715       copySource.redraw();
716       Toast.makeText(ConsoleActivity.this, getString(R.string.terminal_copy_start),
717           Toast.LENGTH_LONG).show();
718       return true;
719     } else if (itemId == MenuId.PASTE.getId()) {
720       TerminalView terminalView = (TerminalView) findCurrentView(R.id.console_flip);
721       TerminalBridge bridge = terminalView.bridge;
722       // pull string from clipboard and generate all events to force down
723       String clip = clipboard.getText().toString();
724       bridge.injectString(clip);
725       return true;
726     }
727     return false;
728   }
729 
730   @Override
onStart()731   public void onStart() {
732     super.onStart();
733     // connect with manager service to find all bridges
734     // when connected it will insert all views
735     bindService(new Intent(this, ScriptingLayerService.class), mConnection, 0);
736   }
737 
738   @Override
onPause()739   public void onPause() {
740     super.onPause();
741     Log.d("onPause called");
742 
743     // Allow the screen to dim and fall asleep.
744     if (wakelock != null && wakelock.isHeld()) {
745       wakelock.release();
746     }
747 
748     if (forcedOrientation && manager != null) {
749       manager.setResizeAllowed(false);
750     }
751   }
752 
753   @Override
onResume()754   public void onResume() {
755     super.onResume();
756     Log.d("onResume called");
757 
758     // Make sure we don't let the screen fall asleep.
759     // This also keeps the Wi-Fi chipset from disconnecting us.
760     if (wakelock != null && prefs.getBoolean(PreferenceConstants.KEEP_ALIVE, true)) {
761       wakelock.acquire();
762     }
763 
764     configureOrientation();
765 
766     if (forcedOrientation && manager != null) {
767       manager.setResizeAllowed(true);
768     }
769   }
770 
771   /*
772    * (non-Javadoc)
773    *
774    * @see android.app.Activity#onNewIntent(android.content.Intent)
775    */
776   @Override
onNewIntent(Intent intent)777   protected void onNewIntent(Intent intent) {
778     super.onNewIntent(intent);
779 
780     Log.d("onNewIntent called");
781 
782     int id = intent.getIntExtra(Constants.EXTRA_PROXY_PORT, -1);
783 
784     if (id > 0) {
785       processID = id;
786     }
787 
788     if (processID == null) {
789       Log.e("Got null intent data in onNewIntent()");
790       return;
791     }
792 
793     if (manager == null) {
794       Log.e("We're not bound in onNewIntent()");
795       return;
796     }
797 
798     TerminalBridge requestedBridge = manager.getConnectedBridge(processID);
799     int requestedIndex = 0;
800 
801     synchronized (flip) {
802       if (requestedBridge == null) {
803         // If we didn't find the requested connection, try opening it
804 
805         try {
806           Log.d(String.format("We couldnt find an existing bridge with id = %d,"
807               + "so creating one now", processID));
808           requestedBridge = manager.openConnection(processID);
809         } catch (Exception e) {
810           Log.e("Problem while trying to create new requested bridge", e);
811         }
812 
813         requestedIndex = addNewTerminalView(requestedBridge);
814       } else {
815         final int flipIndex = getFlipIndex(requestedBridge);
816         if (flipIndex > requestedIndex) {
817           requestedIndex = flipIndex;
818         }
819       }
820 
821       setDisplayedTerminal(requestedIndex);
822     }
823   }
824 
825   @Override
onStop()826   public void onStop() {
827     super.onStop();
828     unbindService(mConnection);
829   }
830 
shiftCurrentTerminal(final int direction)831   protected void shiftCurrentTerminal(final int direction) {
832     View overlay;
833     synchronized (flip) {
834       boolean shouldAnimate = flip.getChildCount() > 1;
835 
836       // Only show animation if there is something else to go to.
837       if (shouldAnimate) {
838         // keep current overlay from popping up again
839         overlay = findCurrentView(R.id.terminal_overlay);
840         if (overlay != null) {
841           overlay.startAnimation(fade_stay_hidden);
842         }
843 
844         if (direction == SHIFT_LEFT) {
845           flip.setInAnimation(slide_left_in);
846           flip.setOutAnimation(slide_left_out);
847           flip.showNext();
848         } else if (direction == SHIFT_RIGHT) {
849           flip.setInAnimation(slide_right_in);
850           flip.setOutAnimation(slide_right_out);
851           flip.showPrevious();
852         }
853       }
854 
855       if (shouldAnimate) {
856         // show overlay on new slide and start fade
857         overlay = findCurrentView(R.id.terminal_overlay);
858         if (overlay != null) {
859           overlay.startAnimation(fade_out_delayed);
860         }
861       }
862 
863       updatePromptVisible();
864     }
865   }
866 
867   /**
868    * Show any prompts requested by the currently visible {@link TerminalView}.
869    */
updatePromptVisible()870   protected void updatePromptVisible() {
871     // check if our currently-visible terminalbridge is requesting any prompt services
872     View view = findCurrentView(R.id.console_flip);
873 
874     // Hide all the prompts in case a prompt request was canceled
875     hideAllPrompts();
876 
877     if (!(view instanceof TerminalView)) {
878       // we dont have an active view, so hide any prompts
879       return;
880     }
881 
882     PromptHelper prompt = ((TerminalView) view).bridge.getPromptHelper();
883 
884     if (Boolean.class.equals(prompt.promptRequested)) {
885       booleanPromptGroup.setVisibility(View.VISIBLE);
886       booleanPrompt.setText(prompt.promptHint);
887       booleanYes.requestFocus();
888     } else {
889       hideAllPrompts();
890       view.requestFocus();
891     }
892   }
893 
894   @Override
onConfigurationChanged(Configuration newConfig)895   public void onConfigurationChanged(Configuration newConfig) {
896     super.onConfigurationChanged(newConfig);
897 
898     Log.d(String.format(
899         "onConfigurationChanged; requestedOrientation=%d, newConfig.orientation=%d",
900         getRequestedOrientation(), newConfig.orientation));
901     if (manager != null) {
902       if (forcedOrientation
903           && (newConfig.orientation != Configuration.ORIENTATION_LANDSCAPE && getRequestedOrientation() == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE)
904           || (newConfig.orientation != Configuration.ORIENTATION_PORTRAIT && getRequestedOrientation() == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT)) {
905         manager.setResizeAllowed(false);
906       } else {
907         manager.setResizeAllowed(true);
908       }
909 
910       manager
911           .setHardKeyboardHidden(newConfig.hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_YES);
912     }
913   }
914 
915   /**
916    * Adds a new TerminalBridge to the current set of views in our ViewFlipper.
917    *
918    * @param bridge
919    *          TerminalBridge to add to our ViewFlipper
920    * @return the child index of the new view in the ViewFlipper
921    */
addNewTerminalView(TerminalBridge bridge)922   private int addNewTerminalView(TerminalBridge bridge) {
923     // let them know about our prompt handler services
924     bridge.getPromptHelper().setHandler(promptHandler);
925 
926     // inflate each terminal view
927     RelativeLayout view = (RelativeLayout) inflater.inflate(R.layout.item_terminal, flip, false);
928 
929     // set the terminal overlay text
930     TextView overlay = (TextView) view.findViewById(R.id.terminal_overlay);
931     overlay.setText(bridge.getName());
932 
933     // and add our terminal view control, using index to place behind overlay
934     TerminalView terminal = new TerminalView(ConsoleActivity.this, bridge);
935     terminal.setId(R.id.console_flip);
936     view.addView(terminal, 0);
937 
938     synchronized (flip) {
939       // finally attach to the flipper
940       flip.addView(view);
941       return flip.getChildCount() - 1;
942     }
943   }
944 
getFlipIndex(TerminalBridge bridge)945   private int getFlipIndex(TerminalBridge bridge) {
946     synchronized (flip) {
947       final int children = flip.getChildCount();
948       for (int i = 0; i < children; i++) {
949         final View view = flip.getChildAt(i).findViewById(R.id.console_flip);
950 
951         if (view == null || !(view instanceof TerminalView)) {
952           // How did that happen?
953           continue;
954         }
955 
956         final TerminalView tv = (TerminalView) view;
957 
958         if (tv.bridge == bridge) {
959           return i;
960         }
961       }
962     }
963 
964     return -1;
965   }
966 
967   /**
968    * Displays the child in the ViewFlipper at the requestedIndex and updates the prompts.
969    *
970    * @param requestedIndex
971    *          the index of the terminal view to display
972    */
setDisplayedTerminal(int requestedIndex)973   private void setDisplayedTerminal(int requestedIndex) {
974     synchronized (flip) {
975       try {
976         // show the requested bridge if found, also fade out overlay
977         flip.setDisplayedChild(requestedIndex);
978         flip.getCurrentView().findViewById(R.id.terminal_overlay).startAnimation(fade_out_delayed);
979       } catch (NullPointerException npe) {
980         Log.d("View went away when we were about to display it", npe);
981       }
982       updatePromptVisible();
983     }
984   }
985 }
986