2 * ConnectBot: simple, powerful, open-source SSH client for Android
3 * Copyright 2007 Kenny Root, Jeffrey Sharkey
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
9 * http://www.apache.org/licenses/LICENSE-2.0
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.
18 package org.connectbot;
20 import java.lang.ref.WeakReference;
21 import java.util.List;
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;
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;
79 import com.nullwire.trace.ExceptionHandler;
81 import de.mud.terminal.vt320;
83 public class ConsoleActivity extends Activity {
84 public final static String TAG = "ConnectBot.ConsoleActivity";
86 protected static final int REQUEST_EDIT = 1;
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;
92 // Direction to shift the ViewFlipper
93 private static final int SHIFT_LEFT = 0;
94 private static final int SHIFT_RIGHT = 1;
96 protected ViewFlipper flip = null;
97 protected TerminalManager bound = null;
98 protected LayoutInflater inflater = null;
100 private SharedPreferences prefs = null;
102 private PowerManager.WakeLock wakelock = null;
104 protected Uri requested;
106 protected ClipboardManager clipboard;
107 private RelativeLayout stringPromptGroup;
108 protected EditText stringPrompt;
109 private TextView stringPromptInstructions;
111 private RelativeLayout booleanPromptGroup;
112 private TextView booleanPrompt;
113 private Button booleanYes, booleanNo;
115 private TextView empty;
117 private Animation slide_left_in, slide_left_out, slide_right_in, slide_right_out, fade_stay_hidden, fade_out_delayed;
119 private Animation keyboard_fade_in, keyboard_fade_out;
120 private float lastX, lastY;
122 private InputMethodManager inputManager;
124 private MenuItem disconnect, copy, paste, portForward, resize, urlscan;
126 protected TerminalBridge copySource = null;
127 private int lastTouchRow, lastTouchCol;
129 private boolean forcedOrientation;
131 private Handler handler = new Handler();
133 private ImageView mKeyboardButton;
135 private ServiceConnection connection = new ServiceConnection() {
136 public void onServiceConnected(ComponentName className, IBinder service) {
137 bound = ((TerminalManager.TerminalBinder) service).getService();
139 // let manager know about our event handling services
140 bound.disconnectHandler = disconnectHandler;
142 Log.d(TAG, String.format("Connected to TerminalManager and found bridges.size=%d", bound.bridges.size()));
144 bound.setResizeAllowed(true);
146 // clear out any existing bridges and record requested index
147 flip.removeAllViews();
149 final String requestedNickname = (requested != null) ? requested.getFragment() : null;
150 int requestedIndex = 0;
152 TerminalBridge requestedBridge = bound.getConnectedBridge(requestedNickname);
154 // If we didn't find the requested connection, try opening it
155 if (requestedNickname != null && requestedBridge == null) {
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);
164 // create views for all bridges on this service
165 for (TerminalBridge bridge : bound.bridges) {
167 final int currentIndex = addNewTerminalView(bridge);
169 // check to see if this bridge was requested
170 if (bridge == requestedBridge)
171 requestedIndex = currentIndex;
174 setDisplayedTerminal(requestedIndex);
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);
184 flip.removeAllViews();
185 updateEmptyVisible();
190 protected Handler promptHandler = new Handler() {
192 public void handleMessage(Message msg) {
193 // someone below us requested to display a prompt
194 updatePromptVisible();
198 protected Handler disconnectHandler = new Handler() {
200 public void handleMessage(Message msg) {
201 Log.d(TAG, "Someone sending HANDLE_DISCONNECT to parentHandler");
203 // someone below us requested to display a password dialog
204 // they are sending nickname and requested
205 TerminalBridge bridge = (TerminalBridge)msg.obj;
207 if (bridge.isAwaitingClose())
215 private void closeBridge(final TerminalBridge bridge) {
216 synchronized (flip) {
217 final int flipIndex = getFlipIndex(bridge);
219 if (flipIndex >= 0) {
220 if (flip.getDisplayedChild() == flipIndex) {
221 shiftCurrentTerminal(SHIFT_LEFT);
223 flip.removeViewAt(flipIndex);
225 /* TODO Remove this workaround when ViewFlipper is fixed to listen
226 * to view removals. Android Issue 1784
228 final int numChildren = flip.getChildCount();
229 if (flip.getDisplayedChild() >= numChildren &&
231 flip.setDisplayedChild(numChildren - 1);
234 updateEmptyVisible();
237 // If we just closed the last bridge, go back to the previous activity.
238 if (flip.getChildCount() == 0) {
244 protected View findCurrentView(int id) {
245 View view = flip.getCurrentView();
246 if(view == null) return null;
247 return view.findViewById(id);
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;
256 protected void hideAllPrompts() {
257 stringPromptGroup.setVisibility(View.GONE);
258 booleanPromptGroup.setVisibility(View.GONE);
262 public void onCreate(Bundle icicle) {
263 super.onCreate(icicle);
265 this.setContentView(R.layout.act_console);
267 ExceptionHandler.register(this);
269 clipboard = (ClipboardManager)getSystemService(CLIPBOARD_SERVICE);
270 prefs = PreferenceManager.getDefaultSharedPreferences(this);
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);
278 // TODO find proper way to disable volume key beep if it exists.
279 setVolumeControlStream(AudioManager.STREAM_MUSIC);
281 PowerManager manager = (PowerManager)getSystemService(Context.POWER_SERVICE);
282 wakelock = manager.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK, TAG);
284 // handle requested console from incoming intent
285 requested = getIntent().getData();
287 inflater = LayoutInflater.from(this);
289 flip = (ViewFlipper)findViewById(R.id.console_flip);
290 empty = (TextView)findViewById(android.R.id.empty);
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;
300 // pass collected password down to current terminal
301 String value = stringPrompt.getText().toString();
303 PromptHelper helper = getCurrentPromptHelper();
304 if(helper == null) return false;
305 helper.setResponse(value);
307 // finally clear password for next user
308 stringPrompt.setText("");
309 updatePromptVisible();
315 booleanPromptGroup = (RelativeLayout) findViewById(R.id.console_boolean_group);
316 booleanPrompt = (TextView)findViewById(R.id.console_prompt);
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();
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();
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);
344 fade_out_delayed = AnimationUtils.loadAnimation(this, R.anim.fade_out_delayed);
345 fade_stay_hidden = AnimationUtils.loadAnimation(this, R.anim.fade_stay_hidden);
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);
351 inputManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
353 final RelativeLayout keyboardGroup = (RelativeLayout) findViewById(R.id.keyboard_group);
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);
362 inputManager.showSoftInput(flip, InputMethodManager.SHOW_FORCED);
363 keyboardGroup.setVisibility(View.GONE);
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;
374 TerminalKeyListener handler = terminal.bridge.getKeyHandler();
375 handler.metaPress(TerminalKeyListener.META_CTRL_ON);
377 keyboardGroup.setVisibility(View.GONE);
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;
388 TerminalKeyListener handler = terminal.bridge.getKeyHandler();
389 handler.sendEscape();
391 keyboardGroup.setVisibility(View.GONE);
395 // detect fling gestures to switch between terminals
396 final GestureDetector detect = new GestureDetector(new GestureDetector.SimpleOnGestureListener() {
397 private float totalY = 0;
400 public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
402 final float distx = e2.getRawX() - e1.getRawX();
403 final float disty = e2.getRawY() - e1.getRawY();
404 final int goalwidth = flip.getWidth() / 2;
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);
414 if (distx < -goalwidth) {
415 shiftCurrentTerminal(SHIFT_LEFT);
426 public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
428 // if copying, then ignore
429 if (copySource != null && copySource.isSelectingForCopy())
432 if (e1 == null || e2 == null)
435 // if releasing then reset total scroll
436 if (e2.getAction() == MotionEvent.ACTION_UP) {
440 // activate consider if within x tolerance
441 if (Math.abs(e1.getX() - e2.getX()) < ViewConfiguration.getTouchSlop() * 4) {
443 View flip = findCurrentView(R.id.console_flip);
444 if(flip == null) return false;
445 TerminalView terminal = (TerminalView)flip;
447 // estimate how many rows we have scrolled through
448 // accumulate distance that doesn't trigger immediate scroll
450 final int moved = (int)(totalY / terminal.bridge.charHeight);
452 // consume as scrollback only if towards right half of screen
453 if (e2.getX() > flip.getWidth() / 2) {
455 int base = terminal.bridge.buffer.getWindowBase();
456 terminal.bridge.buffer.setWindowBase(base + moved);
461 // otherwise consume as pgup/pgdown for every 5 lines
463 ((vt320)terminal.bridge.buffer).keyPressed(vt320.KEY_PAGE_DOWN, ' ', 0);
464 terminal.bridge.tryKeyVibrate();
467 } else if (moved < -5) {
468 ((vt320)terminal.bridge.buffer).keyPressed(vt320.KEY_PAGE_UP, ' ', 0);
469 terminal.bridge.tryKeyVibrate();
484 flip.setLongClickable(true);
485 flip.setOnTouchListener(new OnTouchListener() {
487 public boolean onTouch(View v, MotionEvent event) {
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);
494 SelectionArea area = copySource.getSelectionArea();
496 switch(event.getAction()) {
497 case MotionEvent.ACTION_DOWN:
498 // recording starting area
499 if (area.isSelectingOrigin()) {
507 case MotionEvent.ACTION_MOVE:
508 /* ignore when user hasn't moved since last time so
509 * we can fine-tune with directional pad
511 if (row == lastTouchRow && col == lastTouchCol)
514 // if the user moves, start the selection for other corner
515 area.finishSelectingOrigin();
517 // update selected area
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.
528 if (area.getLeft() == area.getRight() &&
529 area.getTop() == area.getBottom()) {
533 // copy selected area to clipboard
534 String copiedText = area.copyFrom(copySource.buffer);
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
540 case MotionEvent.ACTION_CANCEL:
541 // make sure we clear any highlighted area
543 copySource.setSelectingForCopy(false);
549 Configuration config = getResources().getConfiguration();
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);
562 handler.postDelayed(new Runnable() {
564 if (keyboardGroup.getVisibility() == View.GONE)
567 keyboardGroup.startAnimation(keyboard_fade_out);
568 keyboardGroup.setVisibility(View.GONE);
570 }, KEYBOARD_DISPLAY_TIME);
573 // pass any touch events back to detector
574 return detect.onTouchEvent(event);
584 private void configureOrientation() {
585 String rotateDefault;
586 if (getResources().getConfiguration().keyboard == Configuration.KEYBOARD_NOKEYS)
587 rotateDefault = PreferenceConstants.ROTATION_PORTRAIT;
589 rotateDefault = PreferenceConstants.ROTATION_LANDSCAPE;
591 String rotate = prefs.getString(PreferenceConstants.ROTATION, rotateDefault);
592 if (PreferenceConstants.ROTATION_DEFAULT.equals(rotate))
593 rotate = rotateDefault;
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;
603 setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
604 forcedOrientation = false;
610 public boolean onCreateOptionsMenu(Menu menu) {
611 super.onCreateOptionsMenu(menu);
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;
619 if (activeTerminal) {
620 TerminalBridge bridge = ((TerminalView) view).bridge;
621 sessionOpen = bridge.isSessionOpen();
622 disconnected = bridge.isDisconnected();
623 canForwardPorts = bridge.canFowardPorts();
626 menu.setQwertyMode(true);
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;
640 bridge.dispatchDisconnect(true);
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;
655 SelectionArea area = copySource.getSelectionArea();
657 area.setBounds(copySource.buffer.getColumns(), copySource.buffer.getRows());
659 copySource.setSelectingForCopy(true);
661 // Make sure we show the initial selection
664 Toast.makeText(ConsoleActivity.this, getString(R.string.console_copy_start), Toast.LENGTH_LONG).show();
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;
679 // pull string from clipboard and generate all events to force down
680 String clip = clipboard.getText().toString();
681 bridge.injectString(clip);
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;
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);
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);
711 List<String> urls = terminalView.bridge.scanForURLs();
713 Dialog urlDialog = new Dialog(ConsoleActivity.this);
714 urlDialog.setTitle(R.string.console_menu_urlscan);
716 ListView urlListView = new ListView(ConsoleActivity.this);
717 URLItemListener urlListener = new URLItemListener(ConsoleActivity.this);
718 urlListView.setOnItemClickListener(urlListener);
720 urlListView.setAdapter(new ArrayAdapter<String>(ConsoleActivity.this, android.R.layout.simple_list_item_1, urls));
721 urlDialog.setContentView(urlListView);
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);
736 final View resizeView = inflater.inflate(R.layout.dia_resize, null, false);
737 new AlertDialog.Builder(ConsoleActivity.this)
739 .setPositiveButton(R.string.button_resize, new DialogInterface.OnClickListener() {
740 public void onClick(DialogInterface dialog, int which) {
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.
755 terminalView.forceSize(width, height);
757 }).setNegativeButton(android.R.string.cancel, null).create().show();
767 public boolean onPrepareOptionsMenu(Menu menu) {
768 super.onPrepareOptionsMenu(menu);
770 setVolumeControlStream(AudioManager.STREAM_NOTIFICATION);
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;
778 if (activeTerminal) {
779 TerminalBridge bridge = ((TerminalView) view).bridge;
780 sessionOpen = bridge.isSessionOpen();
781 disconnected = bridge.isDisconnected();
782 canForwardPorts = bridge.canFowardPorts();
785 disconnect.setEnabled(activeTerminal);
786 if (sessionOpen || !disconnected)
787 disconnect.setTitle(R.string.list_host_disconnect);
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);
800 public void onOptionsMenuClosed(Menu menu) {
801 super.onOptionsMenuClosed(menu);
803 setVolumeControlStream(AudioManager.STREAM_MUSIC);
807 public void onStart() {
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);
816 public void onPause() {
818 Log.d(TAG, "onPause called");
820 // Allow the screen to dim and fall asleep.
821 if (wakelock != null && wakelock.isHeld())
824 if (forcedOrientation && bound != null)
825 bound.setResizeAllowed(false);
829 public void onResume() {
831 Log.d(TAG, "onResume called");
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))
838 configureOrientation();
840 if (forcedOrientation && bound != null)
841 bound.setResizeAllowed(true);
845 * @see android.app.Activity#onNewIntent(android.content.Intent)
848 protected void onNewIntent(Intent intent) {
849 super.onNewIntent(intent);
851 Log.d(TAG, "onNewIntent called");
853 requested = intent.getData();
855 if (requested == null) {
856 Log.e(TAG, "Got null intent data in onNewIntent()");
861 Log.e(TAG, "We're not bound in onNewIntent()");
865 TerminalBridge requestedBridge = bound.getConnectedBridge(requested.getFragment());
866 int requestedIndex = 0;
868 synchronized (flip) {
869 if (requestedBridge == null) {
870 // If we didn't find the requested connection, try opening it
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);
880 requestedIndex = addNewTerminalView(requestedBridge);
882 final int flipIndex = getFlipIndex(requestedBridge);
883 if (flipIndex > requestedIndex) {
884 requestedIndex = flipIndex;
888 setDisplayedTerminal(requestedIndex);
893 public void onStop() {
896 unbindService(connection);
899 protected void shiftCurrentTerminal(final int direction) {
901 synchronized (flip) {
902 boolean shouldAnimate = flip.getChildCount() > 1;
904 // Only show animation if there is something else to go to.
906 // keep current overlay from popping up again
907 overlay = findCurrentView(R.id.terminal_overlay);
909 overlay.startAnimation(fade_stay_hidden);
911 if (direction == SHIFT_LEFT) {
912 flip.setInAnimation(slide_left_in);
913 flip.setOutAnimation(slide_left_out);
915 } else if (direction == SHIFT_RIGHT) {
916 flip.setInAnimation(slide_right_in);
917 flip.setOutAnimation(slide_right_out);
922 ConsoleActivity.this.updateDefault();
925 // show overlay on new slide and start fade
926 overlay = findCurrentView(R.id.terminal_overlay);
928 overlay.startAnimation(fade_out_delayed);
931 updatePromptVisible();
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
940 private void updateDefault() {
941 // update the current default terminal
942 View view = findCurrentView(R.id.console_flip);
943 if(!(view instanceof TerminalView)) return;
945 TerminalView terminal = (TerminalView)view;
946 if(bound == null) return;
947 bound.defaultBridge = terminal.bridge;
950 protected void updateEmptyVisible() {
951 // update visibility of empty status message
952 empty.setVisibility((flip.getChildCount() == 0) ? View.VISIBLE : View.GONE);
956 * Show any prompts requested by the currently visible {@link TerminalView}.
958 protected void updatePromptVisible() {
959 // check if our currently-visible terminalbridge is requesting any prompt services
960 View view = findCurrentView(R.id.console_flip);
962 // Hide all the prompts in case a prompt request was canceled
965 if(!(view instanceof TerminalView)) {
966 // we dont have an active view, so hide any prompts
970 PromptHelper prompt = ((TerminalView)view).bridge.promptHelper;
971 if(String.class.equals(prompt.promptRequested)) {
972 stringPromptGroup.setVisibility(View.VISIBLE);
974 String instructions = prompt.promptInstructions;
975 if (instructions != null && instructions.length() > 0) {
976 stringPromptInstructions.setVisibility(View.VISIBLE);
977 stringPromptInstructions.setText(instructions);
979 stringPromptInstructions.setVisibility(View.GONE);
980 stringPrompt.setText("");
981 stringPrompt.setHint(prompt.promptHint);
982 stringPrompt.requestFocus();
984 } else if(Boolean.class.equals(prompt.promptRequested)) {
985 booleanPromptGroup.setVisibility(View.VISIBLE);
986 booleanPrompt.setText(prompt.promptHint);
987 booleanYes.requestFocus();
995 private class URLItemListener implements OnItemClickListener {
996 private WeakReference<Context> contextRef;
998 URLItemListener(Context context) {
999 this.contextRef = new WeakReference<Context>(context);
1002 public void onItemClick(AdapterView<?> arg0, View view, int position, long id) {
1003 Context context = contextRef.get();
1005 if (context == null)
1009 TextView urlView = (TextView) view;
1011 String url = urlView.getText().toString();
1012 if (url.indexOf("://") < 0)
1013 url = "http://" + url;
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...
1026 public void onConfigurationChanged(Configuration newConfig) {
1027 super.onConfigurationChanged(newConfig);
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);
1038 bound.setResizeAllowed(true);
1040 bound.hardKeyboardHidden = (newConfig.hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_YES);
1042 mKeyboardButton.setVisibility(bound.hardKeyboardHidden ? View.VISIBLE : View.GONE);
1047 * Adds a new TerminalBridge to the current set of views in our ViewFlipper.
1049 * @param bridge TerminalBridge to add to our ViewFlipper
1050 * @return the child index of the new view in the ViewFlipper
1052 private int addNewTerminalView(TerminalBridge bridge) {
1053 // let them know about our prompt handler services
1054 bridge.promptHelper.setHandler(promptHandler);
1056 // inflate each terminal view
1057 RelativeLayout view = (RelativeLayout)inflater.inflate(R.layout.item_terminal, flip, false);
1059 // set the terminal overlay text
1060 TextView overlay = (TextView)view.findViewById(R.id.terminal_overlay);
1061 overlay.setText(bridge.host.getNickname());
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);
1068 synchronized (flip) {
1069 // finally attach to the flipper
1071 return flip.getChildCount() - 1;
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);
1081 if (view == null || !(view instanceof TerminalView)) {
1082 // How did that happen?
1086 final TerminalView tv = (TerminalView) view;
1088 if (tv.bridge == bridge) {
1098 * Displays the child in the ViewFlipper at the requestedIndex and updates the prompts.
1100 * @param requestedIndex the index of the terminal view to display
1102 private void setDisplayedTerminal(int requestedIndex) {
1103 synchronized (flip) {
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);
1113 updatePromptVisible();
1114 updateEmptyVisible();