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