OSDN Git Service

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