Home | History | Annotate | Download | only in connectbot
      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;
    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    */
    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 
    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 
    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 
    251   protected void hideAllPrompts() {
    252     booleanPromptGroup.setVisibility(View.GONE);
    253   }
    254 
    255   @Override
    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 
    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
    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
    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
    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
    639   public void onOptionsMenuClosed(Menu menu) {
    640     super.onOptionsMenuClosed(menu);
    641     setVolumeControlStream(AudioManager.STREAM_MUSIC);
    642   }
    643 
    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 
    667   private void doPreferences() {
    668     startActivity(new Intent(this, Preferences.class));
    669   }
    670 
    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 (at) 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
    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
    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
    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
    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
    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
    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
    825   public void onStop() {
    826     super.onStop();
    827     unbindService(mConnection);
    828   }
    829 
    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    */
    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
    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    */
    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 
    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    */
    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