• 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.service.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         Log.i(String.format("Killing process from ConsoleActivity, %s", intent.toUri(0)));
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