1/*
2 * ConnectBot: simple, powerful, open-source SSH client for Android
3 * Copyright 2007 Kenny Root, Jeffrey Sharkey
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *     http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18/**
19 * @author modified by raaar
20 *
21 */
22
23package org.connectbot;
24
25import android.app.Activity;
26import android.app.AlertDialog;
27import android.content.ComponentName;
28import android.content.Context;
29import android.content.DialogInterface;
30import android.content.Intent;
31import android.content.ServiceConnection;
32import android.content.SharedPreferences;
33import android.content.pm.ActivityInfo;
34import android.content.res.Configuration;
35import android.media.AudioManager;
36import android.net.Uri;
37import android.os.Bundle;
38import android.os.Handler;
39import android.os.IBinder;
40import android.os.Message;
41import android.os.PowerManager;
42import android.preference.PreferenceManager;
43import android.text.ClipboardManager;
44import android.view.ContextMenu;
45import android.view.ContextMenu.ContextMenuInfo;
46import android.view.GestureDetector;
47import android.view.LayoutInflater;
48import android.view.Menu;
49import android.view.MenuItem;
50import android.view.MotionEvent;
51import android.view.View;
52import android.view.View.OnClickListener;
53import android.view.View.OnTouchListener;
54import android.view.ViewConfiguration;
55import android.view.WindowManager;
56import android.view.animation.Animation;
57import android.view.animation.AnimationUtils;
58import android.view.inputmethod.InputMethodManager;
59import android.widget.Button;
60import android.widget.EditText;
61import android.widget.ImageView;
62import android.widget.RelativeLayout;
63import android.widget.TextView;
64import android.widget.Toast;
65import android.widget.ViewFlipper;
66
67import com.googlecode.android_scripting.Constants;
68import com.googlecode.android_scripting.Log;
69import com.googlecode.android_scripting.R;
70import com.googlecode.android_scripting.ScriptProcess;
71import com.googlecode.android_scripting.activity.Preferences;
72import com.googlecode.android_scripting.activity.ScriptingLayerService;
73
74import de.mud.terminal.VDUBuffer;
75import de.mud.terminal.vt320;
76
77import org.connectbot.service.PromptHelper;
78import org.connectbot.service.TerminalBridge;
79import org.connectbot.service.TerminalManager;
80import org.connectbot.util.PreferenceConstants;
81import org.connectbot.util.SelectionArea;
82
83public class ConsoleActivity extends Activity {
84
85  protected static final int REQUEST_EDIT = 1;
86
87  private static final int CLICK_TIME = 250;
88  private static final float MAX_CLICK_DISTANCE = 25f;
89  private static final int KEYBOARD_DISPLAY_TIME = 1250;
90
91  // Direction to shift the ViewFlipper
92  private static final int SHIFT_LEFT = 0;
93  private static final int SHIFT_RIGHT = 1;
94
95  protected ViewFlipper flip = null;
96  protected TerminalManager manager = null;
97  protected ScriptingLayerService mService = null;
98  protected LayoutInflater inflater = null;
99
100  private SharedPreferences prefs = null;
101
102  private PowerManager.WakeLock wakelock = null;
103
104  protected Integer processID;
105
106  protected ClipboardManager clipboard;
107
108  private RelativeLayout booleanPromptGroup;
109  private TextView booleanPrompt;
110  private Button booleanYes, booleanNo;
111
112  private Animation slide_left_in, slide_left_out, slide_right_in, slide_right_out,
113      fade_stay_hidden, fade_out_delayed;
114
115  private Animation keyboard_fade_in, keyboard_fade_out;
116  private ImageView keyboardButton;
117  private float lastX, lastY;
118
119  private int mTouchSlopSquare;
120
121  private InputMethodManager inputManager;
122
123  protected TerminalBridge copySource = null;
124  private int lastTouchRow, lastTouchCol;
125
126  private boolean forcedOrientation;
127
128  private Handler handler = new Handler();
129
130  private static enum MenuId {
131    EDIT, PREFS, EMAIL, RESIZE, COPY, PASTE;
132    public int getId() {
133      return ordinal() + Menu.FIRST;
134    }
135  }
136
137  private final ServiceConnection mConnection = new ServiceConnection() {
138    @Override
139    public void onServiceConnected(ComponentName name, IBinder service) {
140      mService = ((ScriptingLayerService.LocalBinder) service).getService();
141      manager = mService.getTerminalManager();
142      // let manager know about our event handling services
143      manager.setDisconnectHandler(disconnectHandler);
144
145      Log.d(String.format("Connected to TerminalManager and found bridges.size=%d", manager
146          .getBridgeList().size()));
147
148      manager.setResizeAllowed(true);
149
150      // clear out any existing bridges and record requested index
151      flip.removeAllViews();
152
153      int requestedIndex = 0;
154
155      TerminalBridge requestedBridge = manager.getConnectedBridge(processID);
156
157      // If we didn't find the requested connection, try opening it
158      if (processID != null && requestedBridge == null) {
159        try {
160          Log.d(String.format(
161              "We couldnt find an existing bridge with id = %d, so creating one now", processID));
162          requestedBridge = manager.openConnection(processID);
163        } catch (Exception e) {
164          Log.e("Problem while trying to create new requested bridge", e);
165        }
166      }
167
168      // create views for all bridges on this service
169      for (TerminalBridge bridge : manager.getBridgeList()) {
170
171        final int currentIndex = addNewTerminalView(bridge);
172
173        // check to see if this bridge was requested
174        if (bridge == requestedBridge) {
175          requestedIndex = currentIndex;
176        }
177      }
178
179      setDisplayedTerminal(requestedIndex);
180    }
181
182    @Override
183    public void onServiceDisconnected(ComponentName name) {
184      manager = null;
185      mService = null;
186    }
187  };
188
189  protected Handler promptHandler = new Handler() {
190    @Override
191    public void handleMessage(Message msg) {
192      // someone below us requested to display a prompt
193      updatePromptVisible();
194    }
195  };
196
197  protected Handler disconnectHandler = new Handler() {
198    @Override
199    public void handleMessage(Message msg) {
200      Log.d("Someone sending HANDLE_DISCONNECT to parentHandler");
201      TerminalBridge bridge = (TerminalBridge) msg.obj;
202      closeBridge(bridge);
203    }
204  };
205
206  /**
207   * @param bridge
208   */
209  private void closeBridge(final TerminalBridge bridge) {
210    synchronized (flip) {
211      final int flipIndex = getFlipIndex(bridge);
212
213      if (flipIndex >= 0) {
214        if (flip.getDisplayedChild() == flipIndex) {
215          shiftCurrentTerminal(SHIFT_LEFT);
216        }
217        flip.removeViewAt(flipIndex);
218
219        /*
220         * TODO Remove this workaround when ViewFlipper is fixed to listen to view removals. Android
221         * Issue 1784
222         */
223        final int numChildren = flip.getChildCount();
224        if (flip.getDisplayedChild() >= numChildren && numChildren > 0) {
225          flip.setDisplayedChild(numChildren - 1);
226        }
227      }
228
229      // If we just closed the last bridge, go back to the previous activity.
230      if (flip.getChildCount() == 0) {
231        finish();
232      }
233    }
234  }
235
236  protected View findCurrentView(int id) {
237    View view = flip.getCurrentView();
238    if (view == null) {
239      return null;
240    }
241    return view.findViewById(id);
242  }
243
244  protected PromptHelper getCurrentPromptHelper() {
245    View view = findCurrentView(R.id.console_flip);
246    if (!(view instanceof TerminalView)) {
247      return null;
248    }
249    return ((TerminalView) view).bridge.getPromptHelper();
250  }
251
252  protected void hideAllPrompts() {
253    booleanPromptGroup.setVisibility(View.GONE);
254  }
255
256  @Override
257  public void onCreate(Bundle icicle) {
258    super.onCreate(icicle);
259
260    this.setContentView(R.layout.act_console);
261
262    clipboard = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
263    prefs = PreferenceManager.getDefaultSharedPreferences(this);
264
265    // hide status bar if requested by user
266    if (prefs.getBoolean(PreferenceConstants.FULLSCREEN, false)) {
267      getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
268          WindowManager.LayoutParams.FLAG_FULLSCREEN);
269    }
270
271    // TODO find proper way to disable volume key beep if it exists.
272    setVolumeControlStream(AudioManager.STREAM_MUSIC);
273
274    PowerManager manager = (PowerManager) getSystemService(Context.POWER_SERVICE);
275    wakelock = manager.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK, getPackageName());
276
277    // handle requested console from incoming intent
278    int id = getIntent().getIntExtra(Constants.EXTRA_PROXY_PORT, -1);
279
280    if (id > 0) {
281      processID = id;
282    }
283
284    inflater = LayoutInflater.from(this);
285
286    flip = (ViewFlipper) findViewById(R.id.console_flip);
287    booleanPromptGroup = (RelativeLayout) findViewById(R.id.console_boolean_group);
288    booleanPrompt = (TextView) findViewById(R.id.console_prompt);
289
290    booleanYes = (Button) findViewById(R.id.console_prompt_yes);
291    booleanYes.setOnClickListener(new OnClickListener() {
292      public void onClick(View v) {
293        PromptHelper helper = getCurrentPromptHelper();
294        if (helper == null) {
295          return;
296        }
297        helper.setResponse(Boolean.TRUE);
298        updatePromptVisible();
299      }
300    });
301
302    booleanNo = (Button) findViewById(R.id.console_prompt_no);
303    booleanNo.setOnClickListener(new OnClickListener() {
304      public void onClick(View v) {
305        PromptHelper helper = getCurrentPromptHelper();
306        if (helper == null) {
307          return;
308        }
309        helper.setResponse(Boolean.FALSE);
310        updatePromptVisible();
311      }
312    });
313
314    // preload animations for terminal switching
315    slide_left_in = AnimationUtils.loadAnimation(this, R.anim.slide_left_in);
316    slide_left_out = AnimationUtils.loadAnimation(this, R.anim.slide_left_out);
317    slide_right_in = AnimationUtils.loadAnimation(this, R.anim.slide_right_in);
318    slide_right_out = AnimationUtils.loadAnimation(this, R.anim.slide_right_out);
319
320    fade_out_delayed = AnimationUtils.loadAnimation(this, R.anim.fade_out_delayed);
321    fade_stay_hidden = AnimationUtils.loadAnimation(this, R.anim.fade_stay_hidden);
322
323    // Preload animation for keyboard button
324    keyboard_fade_in = AnimationUtils.loadAnimation(this, R.anim.keyboard_fade_in);
325    keyboard_fade_out = AnimationUtils.loadAnimation(this, R.anim.keyboard_fade_out);
326
327    inputManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
328    keyboardButton = (ImageView) findViewById(R.id.keyboard_button);
329    keyboardButton.setOnClickListener(new OnClickListener() {
330      public void onClick(View view) {
331        View flip = findCurrentView(R.id.console_flip);
332        if (flip == null) {
333          return;
334        }
335
336        inputManager.showSoftInput(flip, InputMethodManager.SHOW_FORCED);
337        keyboardButton.setVisibility(View.GONE);
338      }
339    });
340    if (prefs.getBoolean(PreferenceConstants.HIDE_KEYBOARD, false)) {
341      // Force hidden keyboard.
342      getWindow().setSoftInputMode(
343          WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN
344              | WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
345    }
346    final ViewConfiguration configuration = ViewConfiguration.get(this);
347    int touchSlop = configuration.getScaledTouchSlop();
348    mTouchSlopSquare = touchSlop * touchSlop;
349
350    // detect fling gestures to switch between terminals
351    final GestureDetector detect =
352        new GestureDetector(new GestureDetector.SimpleOnGestureListener() {
353          private float totalY = 0;
354
355          @Override
356          public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
357
358            final float distx = e2.getRawX() - e1.getRawX();
359            final float disty = e2.getRawY() - e1.getRawY();
360            final int goalwidth = flip.getWidth() / 2;
361
362            // need to slide across half of display to trigger console change
363            // make sure user kept a steady hand horizontally
364            if (Math.abs(disty) < (flip.getHeight() / 4)) {
365              if (distx > goalwidth) {
366                shiftCurrentTerminal(SHIFT_RIGHT);
367                return true;
368              }
369
370              if (distx < -goalwidth) {
371                shiftCurrentTerminal(SHIFT_LEFT);
372                return true;
373              }
374
375            }
376
377            return false;
378          }
379
380          @Override
381          public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
382
383            // if copying, then ignore
384            if (copySource != null && copySource.isSelectingForCopy()) {
385              return false;
386            }
387
388            if (e1 == null || e2 == null) {
389              return false;
390            }
391
392            // if releasing then reset total scroll
393            if (e2.getAction() == MotionEvent.ACTION_UP) {
394              totalY = 0;
395            }
396
397            // activate consider if within x tolerance
398            if (Math.abs(e1.getX() - e2.getX()) < ViewConfiguration.getTouchSlop() * 4) {
399
400              View flip = findCurrentView(R.id.console_flip);
401              if (flip == null) {
402                return false;
403              }
404              TerminalView terminal = (TerminalView) flip;
405
406              // estimate how many rows we have scrolled through
407              // accumulate distance that doesn't trigger immediate scroll
408              totalY += distanceY;
409              final int moved = (int) (totalY / terminal.bridge.charHeight);
410
411              VDUBuffer buffer = terminal.bridge.getVDUBuffer();
412
413              // consume as scrollback only if towards right half of screen
414              if (e2.getX() > flip.getWidth() / 2) {
415                if (moved != 0) {
416                  int base = buffer.getWindowBase();
417                  buffer.setWindowBase(base + moved);
418                  totalY = 0;
419                  return true;
420                }
421              } else {
422                // otherwise consume as pgup/pgdown for every 5 lines
423                if (moved > 5) {
424                  ((vt320) buffer).keyPressed(vt320.KEY_PAGE_DOWN, ' ', 0);
425                  terminal.bridge.tryKeyVibrate();
426                  totalY = 0;
427                  return true;
428                } else if (moved < -5) {
429                  ((vt320) buffer).keyPressed(vt320.KEY_PAGE_UP, ' ', 0);
430                  terminal.bridge.tryKeyVibrate();
431                  totalY = 0;
432                  return true;
433                }
434
435              }
436
437            }
438
439            return false;
440          }
441
442        });
443
444    flip.setOnCreateContextMenuListener(this);
445
446    flip.setOnTouchListener(new OnTouchListener() {
447
448      public boolean onTouch(View v, MotionEvent event) {
449
450        // when copying, highlight the area
451        if (copySource != null && copySource.isSelectingForCopy()) {
452          int row = (int) Math.floor(event.getY() / copySource.charHeight);
453          int col = (int) Math.floor(event.getX() / copySource.charWidth);
454
455          SelectionArea area = copySource.getSelectionArea();
456
457          switch (event.getAction()) {
458          case MotionEvent.ACTION_DOWN:
459            // recording starting area
460            if (area.isSelectingOrigin()) {
461              area.setRow(row);
462              area.setColumn(col);
463              lastTouchRow = row;
464              lastTouchCol = col;
465              copySource.redraw();
466            }
467            return true;
468          case MotionEvent.ACTION_MOVE:
469            /*
470             * ignore when user hasn't moved since last time so we can fine-tune with directional
471             * pad
472             */
473            if (row == lastTouchRow && col == lastTouchCol) {
474              return true;
475            }
476            // if the user moves, start the selection for other corner
477            area.finishSelectingOrigin();
478
479            // update selected area
480            area.setRow(row);
481            area.setColumn(col);
482            lastTouchRow = row;
483            lastTouchCol = col;
484            copySource.redraw();
485            return true;
486          case MotionEvent.ACTION_UP:
487            /*
488             * If they didn't move their finger, maybe they meant to select the rest of the text
489             * with the directional pad.
490             */
491            if (area.getLeft() == area.getRight() && area.getTop() == area.getBottom()) {
492              return true;
493            }
494
495            // copy selected area to clipboard
496            String copiedText = area.copyFrom(copySource.getVDUBuffer());
497
498            clipboard.setText(copiedText);
499            Toast.makeText(ConsoleActivity.this,
500                getString(R.string.terminal_copy_done, copiedText.length()), Toast.LENGTH_LONG)
501                .show();
502            // fall through to clear state
503
504          case MotionEvent.ACTION_CANCEL:
505            // make sure we clear any highlighted area
506            area.reset();
507            copySource.setSelectingForCopy(false);
508            copySource.redraw();
509            return true;
510          }
511        }
512
513        Configuration config = getResources().getConfiguration();
514
515        if (event.getAction() == MotionEvent.ACTION_DOWN) {
516          lastX = event.getX();
517          lastY = event.getY();
518        } else if (event.getAction() == MotionEvent.ACTION_MOVE) {
519          final int deltaX = (int) (lastX - event.getX());
520          final int deltaY = (int) (lastY - event.getY());
521          int distance = (deltaX * deltaX) + (deltaY * deltaY);
522          if (distance > mTouchSlopSquare) {
523            // If currently scheduled long press event is not canceled here,
524            // GestureDetector.onScroll is executed, which takes a while, and by the time we are
525            // back in the view's dispatchTouchEvent
526            // mPendingCheckForLongPress is already executed
527            flip.cancelLongPress();
528          }
529        } else if (event.getAction() == MotionEvent.ACTION_UP) {
530          // Same as above, except now GestureDetector.onFling is called.
531          flip.cancelLongPress();
532          if (config.hardKeyboardHidden != Configuration.KEYBOARDHIDDEN_NO
533              && keyboardButton.getVisibility() == View.GONE
534              && event.getEventTime() - event.getDownTime() < CLICK_TIME
535              && Math.abs(event.getX() - lastX) < MAX_CLICK_DISTANCE
536              && Math.abs(event.getY() - lastY) < MAX_CLICK_DISTANCE) {
537            keyboardButton.startAnimation(keyboard_fade_in);
538            keyboardButton.setVisibility(View.VISIBLE);
539
540            handler.postDelayed(new Runnable() {
541              public void run() {
542                if (keyboardButton.getVisibility() == View.GONE) {
543                  return;
544                }
545
546                keyboardButton.startAnimation(keyboard_fade_out);
547                keyboardButton.setVisibility(View.GONE);
548              }
549            }, KEYBOARD_DISPLAY_TIME);
550
551            return false;
552          }
553        }
554        // pass any touch events back to detector
555        return detect.onTouchEvent(event);
556      }
557
558    });
559
560  }
561
562  private void configureOrientation() {
563    String rotateDefault;
564    if (getResources().getConfiguration().keyboard == Configuration.KEYBOARD_NOKEYS) {
565      rotateDefault = PreferenceConstants.ROTATION_PORTRAIT;
566    } else {
567      rotateDefault = PreferenceConstants.ROTATION_LANDSCAPE;
568    }
569
570    String rotate = prefs.getString(PreferenceConstants.ROTATION, rotateDefault);
571    if (PreferenceConstants.ROTATION_DEFAULT.equals(rotate)) {
572      rotate = rotateDefault;
573    }
574
575    // request a forced orientation if requested by user
576    if (PreferenceConstants.ROTATION_LANDSCAPE.equals(rotate)) {
577      setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
578      forcedOrientation = true;
579    } else if (PreferenceConstants.ROTATION_PORTRAIT.equals(rotate)) {
580      setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
581      forcedOrientation = true;
582    } else {
583      setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
584      forcedOrientation = false;
585    }
586  }
587
588  @Override
589  public boolean onCreateOptionsMenu(Menu menu) {
590    super.onCreateOptionsMenu(menu);
591    getMenuInflater().inflate(R.menu.terminal, menu);
592    menu.setQwertyMode(true);
593    return true;
594  }
595
596  @Override
597  public boolean onPrepareOptionsMenu(Menu menu) {
598    super.onPrepareOptionsMenu(menu);
599    setVolumeControlStream(AudioManager.STREAM_NOTIFICATION);
600    TerminalBridge bridge = ((TerminalView) findCurrentView(R.id.console_flip)).bridge;
601    boolean sessionOpen = bridge.isSessionOpen();
602    menu.findItem(R.id.terminal_menu_resize).setEnabled(sessionOpen);
603    if (bridge.getProcess() instanceof ScriptProcess) {
604      menu.findItem(R.id.terminal_menu_exit_and_edit).setEnabled(true);
605    }
606    bridge.onPrepareOptionsMenu(menu);
607    return true;
608  }
609
610  @Override
611  public boolean onOptionsItemSelected(MenuItem item) {
612    if (item.getItemId() == R.id.terminal_menu_resize) {
613      doResize();
614    } else if (item.getItemId() == R.id.terminal_menu_preferences) {
615      doPreferences();
616    } else if (item.getItemId() == R.id.terminal_menu_send_email) {
617      doEmailTranscript();
618    } else if (item.getItemId() == R.id.terminal_menu_exit_and_edit) {
619      TerminalView terminalView = (TerminalView) findCurrentView(R.id.console_flip);
620      TerminalBridge bridge = terminalView.bridge;
621      if (manager != null) {
622        manager.closeConnection(bridge, true);
623      } else {
624        Intent intent = new Intent(this, ScriptingLayerService.class);
625        intent.setAction(Constants.ACTION_KILL_PROCESS);
626        intent.putExtra(Constants.EXTRA_PROXY_PORT, bridge.getId());
627        startService(intent);
628        Message.obtain(disconnectHandler, -1, bridge).sendToTarget();
629      }
630      Intent intent = new Intent(Constants.ACTION_EDIT_SCRIPT);
631      ScriptProcess process = (ScriptProcess) bridge.getProcess();
632      intent.putExtra(Constants.EXTRA_SCRIPT_PATH, process.getPath());
633      startActivity(intent);
634      finish();
635    }
636    return super.onOptionsItemSelected(item);
637  }
638
639  @Override
640  public void onOptionsMenuClosed(Menu menu) {
641    super.onOptionsMenuClosed(menu);
642    setVolumeControlStream(AudioManager.STREAM_MUSIC);
643  }
644
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
668  private void doPreferences() {
669    startActivity(new Intent(this, Preferences.class));
670  }
671
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
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
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
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
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
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
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
826  public void onStop() {
827    super.onStop();
828    unbindService(mConnection);
829  }
830
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   */
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
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   */
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
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   */
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