1 /* 2 * ConnectBot: simple, powerful, open-source SSH client for Android 3 * Copyright 2007 Kenny Root, Jeffrey Sharkey 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18 /** 19 * 20 */ 21 22 package org.connectbot; 23 24 import android.app.Activity; 25 import android.app.AlertDialog; 26 import android.content.ComponentName; 27 import android.content.Context; 28 import android.content.DialogInterface; 29 import android.content.Intent; 30 import android.content.ServiceConnection; 31 import android.content.SharedPreferences; 32 import android.content.pm.ActivityInfo; 33 import android.content.res.Configuration; 34 import android.media.AudioManager; 35 import android.net.Uri; 36 import android.os.Bundle; 37 import android.os.Handler; 38 import android.os.IBinder; 39 import android.os.Message; 40 import android.os.PowerManager; 41 import android.preference.PreferenceManager; 42 import android.text.ClipboardManager; 43 import android.view.ContextMenu; 44 import android.view.ContextMenu.ContextMenuInfo; 45 import android.view.GestureDetector; 46 import android.view.LayoutInflater; 47 import android.view.Menu; 48 import android.view.MenuItem; 49 import android.view.MotionEvent; 50 import android.view.View; 51 import android.view.View.OnClickListener; 52 import android.view.View.OnTouchListener; 53 import android.view.ViewConfiguration; 54 import android.view.WindowManager; 55 import android.view.animation.Animation; 56 import android.view.animation.AnimationUtils; 57 import android.view.inputmethod.InputMethodManager; 58 import android.widget.Button; 59 import android.widget.EditText; 60 import android.widget.ImageView; 61 import android.widget.RelativeLayout; 62 import android.widget.TextView; 63 import android.widget.Toast; 64 import android.widget.ViewFlipper; 65 66 import com.googlecode.android_scripting.Constants; 67 import com.googlecode.android_scripting.Log; 68 import com.googlecode.android_scripting.R; 69 import com.googlecode.android_scripting.ScriptProcess; 70 import com.googlecode.android_scripting.activity.Preferences; 71 import com.googlecode.android_scripting.activity.ScriptingLayerService; 72 73 import de.mud.terminal.VDUBuffer; 74 import de.mud.terminal.vt320; 75 76 import org.connectbot.service.PromptHelper; 77 import org.connectbot.service.TerminalBridge; 78 import org.connectbot.service.TerminalManager; 79 import org.connectbot.util.PreferenceConstants; 80 import org.connectbot.util.SelectionArea; 81 82 public class ConsoleActivity extends Activity { 83 84 protected static final int REQUEST_EDIT = 1; 85 86 private static final int CLICK_TIME = 250; 87 private static final float MAX_CLICK_DISTANCE = 25f; 88 private static final int KEYBOARD_DISPLAY_TIME = 1250; 89 90 // Direction to shift the ViewFlipper 91 private static final int SHIFT_LEFT = 0; 92 private static final int SHIFT_RIGHT = 1; 93 94 protected ViewFlipper flip = null; 95 protected TerminalManager manager = null; 96 protected ScriptingLayerService mService = null; 97 protected LayoutInflater inflater = null; 98 99 private SharedPreferences prefs = null; 100 101 private PowerManager.WakeLock wakelock = null; 102 103 protected Integer processID; 104 105 protected ClipboardManager clipboard; 106 107 private RelativeLayout booleanPromptGroup; 108 private TextView booleanPrompt; 109 private Button booleanYes, booleanNo; 110 111 private Animation slide_left_in, slide_left_out, slide_right_in, slide_right_out, 112 fade_stay_hidden, fade_out_delayed; 113 114 private Animation keyboard_fade_in, keyboard_fade_out; 115 private ImageView keyboardButton; 116 private float lastX, lastY; 117 118 private int mTouchSlopSquare; 119 120 private InputMethodManager inputManager; 121 122 protected TerminalBridge copySource = null; 123 private int lastTouchRow, lastTouchCol; 124 125 private boolean forcedOrientation; 126 127 private Handler handler = new Handler(); 128 129 private static enum MenuId { 130 EDIT, PREFS, EMAIL, RESIZE, COPY, PASTE; getId()131 public int getId() { 132 return ordinal() + Menu.FIRST; 133 } 134 } 135 136 private final ServiceConnection mConnection = new ServiceConnection() { 137 @Override 138 public void onServiceConnected(ComponentName name, IBinder service) { 139 mService = ((ScriptingLayerService.LocalBinder) service).getService(); 140 manager = mService.getTerminalManager(); 141 // let manager know about our event handling services 142 manager.setDisconnectHandler(disconnectHandler); 143 144 Log.d(String.format("Connected to TerminalManager and found bridges.size=%d", manager 145 .getBridgeList().size())); 146 147 manager.setResizeAllowed(true); 148 149 // clear out any existing bridges and record requested index 150 flip.removeAllViews(); 151 152 int requestedIndex = 0; 153 154 TerminalBridge requestedBridge = manager.getConnectedBridge(processID); 155 156 // If we didn't find the requested connection, try opening it 157 if (processID != null && requestedBridge == null) { 158 try { 159 Log.d(String.format( 160 "We couldnt find an existing bridge with id = %d, so creating one now", processID)); 161 requestedBridge = manager.openConnection(processID); 162 } catch (Exception e) { 163 Log.e("Problem while trying to create new requested bridge", e); 164 } 165 } 166 167 // create views for all bridges on this service 168 for (TerminalBridge bridge : manager.getBridgeList()) { 169 170 final int currentIndex = addNewTerminalView(bridge); 171 172 // check to see if this bridge was requested 173 if (bridge == requestedBridge) { 174 requestedIndex = currentIndex; 175 } 176 } 177 178 setDisplayedTerminal(requestedIndex); 179 } 180 181 @Override 182 public void onServiceDisconnected(ComponentName name) { 183 manager = null; 184 mService = null; 185 } 186 }; 187 188 protected Handler promptHandler = new Handler() { 189 @Override 190 public void handleMessage(Message msg) { 191 // someone below us requested to display a prompt 192 updatePromptVisible(); 193 } 194 }; 195 196 protected Handler disconnectHandler = new Handler() { 197 @Override 198 public void handleMessage(Message msg) { 199 Log.d("Someone sending HANDLE_DISCONNECT to parentHandler"); 200 TerminalBridge bridge = (TerminalBridge) msg.obj; 201 closeBridge(bridge); 202 } 203 }; 204 205 /** 206 * @param bridge 207 */ closeBridge(final TerminalBridge bridge)208 private void closeBridge(final TerminalBridge bridge) { 209 synchronized (flip) { 210 final int flipIndex = getFlipIndex(bridge); 211 212 if (flipIndex >= 0) { 213 if (flip.getDisplayedChild() == flipIndex) { 214 shiftCurrentTerminal(SHIFT_LEFT); 215 } 216 flip.removeViewAt(flipIndex); 217 218 /* 219 * TODO Remove this workaround when ViewFlipper is fixed to listen to view removals. Android 220 * Issue 1784 221 */ 222 final int numChildren = flip.getChildCount(); 223 if (flip.getDisplayedChild() >= numChildren && numChildren > 0) { 224 flip.setDisplayedChild(numChildren - 1); 225 } 226 } 227 228 // If we just closed the last bridge, go back to the previous activity. 229 if (flip.getChildCount() == 0) { 230 finish(); 231 } 232 } 233 } 234 findCurrentView(int id)235 protected View findCurrentView(int id) { 236 View view = flip.getCurrentView(); 237 if (view == null) { 238 return null; 239 } 240 return view.findViewById(id); 241 } 242 getCurrentPromptHelper()243 protected PromptHelper getCurrentPromptHelper() { 244 View view = findCurrentView(R.id.console_flip); 245 if (!(view instanceof TerminalView)) { 246 return null; 247 } 248 return ((TerminalView) view).bridge.getPromptHelper(); 249 } 250 hideAllPrompts()251 protected void hideAllPrompts() { 252 booleanPromptGroup.setVisibility(View.GONE); 253 } 254 255 @Override onCreate(Bundle icicle)256 public void onCreate(Bundle icicle) { 257 super.onCreate(icicle); 258 259 this.setContentView(R.layout.act_console); 260 261 clipboard = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE); 262 prefs = PreferenceManager.getDefaultSharedPreferences(this); 263 264 // hide status bar if requested by user 265 if (prefs.getBoolean(PreferenceConstants.FULLSCREEN, false)) { 266 getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, 267 WindowManager.LayoutParams.FLAG_FULLSCREEN); 268 } 269 270 // TODO find proper way to disable volume key beep if it exists. 271 setVolumeControlStream(AudioManager.STREAM_MUSIC); 272 273 PowerManager manager = (PowerManager) getSystemService(Context.POWER_SERVICE); 274 wakelock = manager.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK, getPackageName()); 275 276 // handle requested console from incoming intent 277 int id = getIntent().getIntExtra(Constants.EXTRA_PROXY_PORT, -1); 278 279 if (id > 0) { 280 processID = id; 281 } 282 283 inflater = LayoutInflater.from(this); 284 285 flip = (ViewFlipper) findViewById(R.id.console_flip); 286 booleanPromptGroup = (RelativeLayout) findViewById(R.id.console_boolean_group); 287 booleanPrompt = (TextView) findViewById(R.id.console_prompt); 288 289 booleanYes = (Button) findViewById(R.id.console_prompt_yes); 290 booleanYes.setOnClickListener(new OnClickListener() { 291 public void onClick(View v) { 292 PromptHelper helper = getCurrentPromptHelper(); 293 if (helper == null) { 294 return; 295 } 296 helper.setResponse(Boolean.TRUE); 297 updatePromptVisible(); 298 } 299 }); 300 301 booleanNo = (Button) findViewById(R.id.console_prompt_no); 302 booleanNo.setOnClickListener(new OnClickListener() { 303 public void onClick(View v) { 304 PromptHelper helper = getCurrentPromptHelper(); 305 if (helper == null) { 306 return; 307 } 308 helper.setResponse(Boolean.FALSE); 309 updatePromptVisible(); 310 } 311 }); 312 313 // preload animations for terminal switching 314 slide_left_in = AnimationUtils.loadAnimation(this, R.anim.slide_left_in); 315 slide_left_out = AnimationUtils.loadAnimation(this, R.anim.slide_left_out); 316 slide_right_in = AnimationUtils.loadAnimation(this, R.anim.slide_right_in); 317 slide_right_out = AnimationUtils.loadAnimation(this, R.anim.slide_right_out); 318 319 fade_out_delayed = AnimationUtils.loadAnimation(this, R.anim.fade_out_delayed); 320 fade_stay_hidden = AnimationUtils.loadAnimation(this, R.anim.fade_stay_hidden); 321 322 // Preload animation for keyboard button 323 keyboard_fade_in = AnimationUtils.loadAnimation(this, R.anim.keyboard_fade_in); 324 keyboard_fade_out = AnimationUtils.loadAnimation(this, R.anim.keyboard_fade_out); 325 326 inputManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); 327 keyboardButton = (ImageView) findViewById(R.id.keyboard_button); 328 keyboardButton.setOnClickListener(new OnClickListener() { 329 public void onClick(View view) { 330 View flip = findCurrentView(R.id.console_flip); 331 if (flip == null) { 332 return; 333 } 334 335 inputManager.showSoftInput(flip, InputMethodManager.SHOW_FORCED); 336 keyboardButton.setVisibility(View.GONE); 337 } 338 }); 339 if (prefs.getBoolean(PreferenceConstants.HIDE_KEYBOARD, false)) { 340 // Force hidden keyboard. 341 getWindow().setSoftInputMode( 342 WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN 343 | WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE); 344 } 345 final ViewConfiguration configuration = ViewConfiguration.get(this); 346 int touchSlop = configuration.getScaledTouchSlop(); 347 mTouchSlopSquare = touchSlop * touchSlop; 348 349 // detect fling gestures to switch between terminals 350 final GestureDetector detect = 351 new GestureDetector(new GestureDetector.SimpleOnGestureListener() { 352 private float totalY = 0; 353 354 @Override 355 public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { 356 357 final float distx = e2.getRawX() - e1.getRawX(); 358 final float disty = e2.getRawY() - e1.getRawY(); 359 final int goalwidth = flip.getWidth() / 2; 360 361 // need to slide across half of display to trigger console change 362 // make sure user kept a steady hand horizontally 363 if (Math.abs(disty) < (flip.getHeight() / 4)) { 364 if (distx > goalwidth) { 365 shiftCurrentTerminal(SHIFT_RIGHT); 366 return true; 367 } 368 369 if (distx < -goalwidth) { 370 shiftCurrentTerminal(SHIFT_LEFT); 371 return true; 372 } 373 374 } 375 376 return false; 377 } 378 379 @Override 380 public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { 381 382 // if copying, then ignore 383 if (copySource != null && copySource.isSelectingForCopy()) { 384 return false; 385 } 386 387 if (e1 == null || e2 == null) { 388 return false; 389 } 390 391 // if releasing then reset total scroll 392 if (e2.getAction() == MotionEvent.ACTION_UP) { 393 totalY = 0; 394 } 395 396 // activate consider if within x tolerance 397 if (Math.abs(e1.getX() - e2.getX()) < ViewConfiguration.getTouchSlop() * 4) { 398 399 View flip = findCurrentView(R.id.console_flip); 400 if (flip == null) { 401 return false; 402 } 403 TerminalView terminal = (TerminalView) flip; 404 405 // estimate how many rows we have scrolled through 406 // accumulate distance that doesn't trigger immediate scroll 407 totalY += distanceY; 408 final int moved = (int) (totalY / terminal.bridge.charHeight); 409 410 VDUBuffer buffer = terminal.bridge.getVDUBuffer(); 411 412 // consume as scrollback only if towards right half of screen 413 if (e2.getX() > flip.getWidth() / 2) { 414 if (moved != 0) { 415 int base = buffer.getWindowBase(); 416 buffer.setWindowBase(base + moved); 417 totalY = 0; 418 return true; 419 } 420 } else { 421 // otherwise consume as pgup/pgdown for every 5 lines 422 if (moved > 5) { 423 ((vt320) buffer).keyPressed(vt320.KEY_PAGE_DOWN, ' ', 0); 424 terminal.bridge.tryKeyVibrate(); 425 totalY = 0; 426 return true; 427 } else if (moved < -5) { 428 ((vt320) buffer).keyPressed(vt320.KEY_PAGE_UP, ' ', 0); 429 terminal.bridge.tryKeyVibrate(); 430 totalY = 0; 431 return true; 432 } 433 434 } 435 436 } 437 438 return false; 439 } 440 441 }); 442 443 flip.setOnCreateContextMenuListener(this); 444 445 flip.setOnTouchListener(new OnTouchListener() { 446 447 public boolean onTouch(View v, MotionEvent event) { 448 449 // when copying, highlight the area 450 if (copySource != null && copySource.isSelectingForCopy()) { 451 int row = (int) Math.floor(event.getY() / copySource.charHeight); 452 int col = (int) Math.floor(event.getX() / copySource.charWidth); 453 454 SelectionArea area = copySource.getSelectionArea(); 455 456 switch (event.getAction()) { 457 case MotionEvent.ACTION_DOWN: 458 // recording starting area 459 if (area.isSelectingOrigin()) { 460 area.setRow(row); 461 area.setColumn(col); 462 lastTouchRow = row; 463 lastTouchCol = col; 464 copySource.redraw(); 465 } 466 return true; 467 case MotionEvent.ACTION_MOVE: 468 /* 469 * ignore when user hasn't moved since last time so we can fine-tune with directional 470 * pad 471 */ 472 if (row == lastTouchRow && col == lastTouchCol) { 473 return true; 474 } 475 // if the user moves, start the selection for other corner 476 area.finishSelectingOrigin(); 477 478 // update selected area 479 area.setRow(row); 480 area.setColumn(col); 481 lastTouchRow = row; 482 lastTouchCol = col; 483 copySource.redraw(); 484 return true; 485 case MotionEvent.ACTION_UP: 486 /* 487 * If they didn't move their finger, maybe they meant to select the rest of the text 488 * with the directional pad. 489 */ 490 if (area.getLeft() == area.getRight() && area.getTop() == area.getBottom()) { 491 return true; 492 } 493 494 // copy selected area to clipboard 495 String copiedText = area.copyFrom(copySource.getVDUBuffer()); 496 497 clipboard.setText(copiedText); 498 Toast.makeText(ConsoleActivity.this, 499 getString(R.string.terminal_copy_done, copiedText.length()), Toast.LENGTH_LONG) 500 .show(); 501 // fall through to clear state 502 503 case MotionEvent.ACTION_CANCEL: 504 // make sure we clear any highlighted area 505 area.reset(); 506 copySource.setSelectingForCopy(false); 507 copySource.redraw(); 508 return true; 509 } 510 } 511 512 Configuration config = getResources().getConfiguration(); 513 514 if (event.getAction() == MotionEvent.ACTION_DOWN) { 515 lastX = event.getX(); 516 lastY = event.getY(); 517 } else if (event.getAction() == MotionEvent.ACTION_MOVE) { 518 final int deltaX = (int) (lastX - event.getX()); 519 final int deltaY = (int) (lastY - event.getY()); 520 int distance = (deltaX * deltaX) + (deltaY * deltaY); 521 if (distance > mTouchSlopSquare) { 522 // If currently scheduled long press event is not canceled here, 523 // GestureDetector.onScroll is executed, which takes a while, and by the time we are 524 // back in the view's dispatchTouchEvent 525 // mPendingCheckForLongPress is already executed 526 flip.cancelLongPress(); 527 } 528 } else if (event.getAction() == MotionEvent.ACTION_UP) { 529 // Same as above, except now GestureDetector.onFling is called. 530 flip.cancelLongPress(); 531 if (config.hardKeyboardHidden != Configuration.KEYBOARDHIDDEN_NO 532 && keyboardButton.getVisibility() == View.GONE 533 && event.getEventTime() - event.getDownTime() < CLICK_TIME 534 && Math.abs(event.getX() - lastX) < MAX_CLICK_DISTANCE 535 && Math.abs(event.getY() - lastY) < MAX_CLICK_DISTANCE) { 536 keyboardButton.startAnimation(keyboard_fade_in); 537 keyboardButton.setVisibility(View.VISIBLE); 538 539 handler.postDelayed(new Runnable() { 540 public void run() { 541 if (keyboardButton.getVisibility() == View.GONE) { 542 return; 543 } 544 545 keyboardButton.startAnimation(keyboard_fade_out); 546 keyboardButton.setVisibility(View.GONE); 547 } 548 }, KEYBOARD_DISPLAY_TIME); 549 550 return false; 551 } 552 } 553 // pass any touch events back to detector 554 return detect.onTouchEvent(event); 555 } 556 557 }); 558 559 } 560 configureOrientation()561 private void configureOrientation() { 562 String rotateDefault; 563 if (getResources().getConfiguration().keyboard == Configuration.KEYBOARD_NOKEYS) { 564 rotateDefault = PreferenceConstants.ROTATION_PORTRAIT; 565 } else { 566 rotateDefault = PreferenceConstants.ROTATION_LANDSCAPE; 567 } 568 569 String rotate = prefs.getString(PreferenceConstants.ROTATION, rotateDefault); 570 if (PreferenceConstants.ROTATION_DEFAULT.equals(rotate)) { 571 rotate = rotateDefault; 572 } 573 574 // request a forced orientation if requested by user 575 if (PreferenceConstants.ROTATION_LANDSCAPE.equals(rotate)) { 576 setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); 577 forcedOrientation = true; 578 } else if (PreferenceConstants.ROTATION_PORTRAIT.equals(rotate)) { 579 setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); 580 forcedOrientation = true; 581 } else { 582 setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED); 583 forcedOrientation = false; 584 } 585 } 586 587 @Override onCreateOptionsMenu(Menu menu)588 public boolean onCreateOptionsMenu(Menu menu) { 589 super.onCreateOptionsMenu(menu); 590 getMenuInflater().inflate(R.menu.terminal, menu); 591 menu.setQwertyMode(true); 592 return true; 593 } 594 595 @Override onPrepareOptionsMenu(Menu menu)596 public boolean onPrepareOptionsMenu(Menu menu) { 597 super.onPrepareOptionsMenu(menu); 598 setVolumeControlStream(AudioManager.STREAM_NOTIFICATION); 599 TerminalBridge bridge = ((TerminalView) findCurrentView(R.id.console_flip)).bridge; 600 boolean sessionOpen = bridge.isSessionOpen(); 601 menu.findItem(R.id.terminal_menu_resize).setEnabled(sessionOpen); 602 if (bridge.getProcess() instanceof ScriptProcess) { 603 menu.findItem(R.id.terminal_menu_exit_and_edit).setEnabled(true); 604 } 605 bridge.onPrepareOptionsMenu(menu); 606 return true; 607 } 608 609 @Override onOptionsItemSelected(MenuItem item)610 public boolean onOptionsItemSelected(MenuItem item) { 611 if (item.getItemId() == R.id.terminal_menu_resize) { 612 doResize(); 613 } else if (item.getItemId() == R.id.terminal_menu_preferences) { 614 doPreferences(); 615 } else if (item.getItemId() == R.id.terminal_menu_send_email) { 616 doEmailTranscript(); 617 } else if (item.getItemId() == R.id.terminal_menu_exit_and_edit) { 618 TerminalView terminalView = (TerminalView) findCurrentView(R.id.console_flip); 619 TerminalBridge bridge = terminalView.bridge; 620 if (manager != null) { 621 manager.closeConnection(bridge, true); 622 } else { 623 Intent intent = new Intent(this, ScriptingLayerService.class); 624 intent.setAction(Constants.ACTION_KILL_PROCESS); 625 intent.putExtra(Constants.EXTRA_PROXY_PORT, bridge.getId()); 626 startService(intent); 627 Message.obtain(disconnectHandler, -1, bridge).sendToTarget(); 628 } 629 Intent intent = new Intent(Constants.ACTION_EDIT_SCRIPT); 630 ScriptProcess process = (ScriptProcess) bridge.getProcess(); 631 intent.putExtra(Constants.EXTRA_SCRIPT_PATH, process.getPath()); 632 startActivity(intent); 633 finish(); 634 } 635 return super.onOptionsItemSelected(item); 636 } 637 638 @Override onOptionsMenuClosed(Menu menu)639 public void onOptionsMenuClosed(Menu menu) { 640 super.onOptionsMenuClosed(menu); 641 setVolumeControlStream(AudioManager.STREAM_MUSIC); 642 } 643 doResize()644 private void doResize() { 645 closeOptionsMenu(); 646 final TerminalView terminalView = (TerminalView) findCurrentView(R.id.console_flip); 647 final View resizeView = inflater.inflate(R.layout.dia_resize, null, false); 648 new AlertDialog.Builder(ConsoleActivity.this).setView(resizeView) 649 .setPositiveButton(R.string.button_resize, new DialogInterface.OnClickListener() { 650 public void onClick(DialogInterface dialog, int which) { 651 int width, height; 652 try { 653 width = 654 Integer.parseInt(((EditText) resizeView.findViewById(R.id.width)).getText() 655 .toString()); 656 height = 657 Integer.parseInt(((EditText) resizeView.findViewById(R.id.height)).getText() 658 .toString()); 659 } catch (NumberFormatException nfe) { 660 return; 661 } 662 terminalView.forceSize(width, height); 663 } 664 }).setNegativeButton(android.R.string.cancel, null).create().show(); 665 } 666 doPreferences()667 private void doPreferences() { 668 startActivity(new Intent(this, Preferences.class)); 669 } 670 doEmailTranscript()671 private void doEmailTranscript() { 672 // Don't really want to supply an address, but currently it's required, 673 // otherwise we get an exception. 674 TerminalView terminalView = (TerminalView) findCurrentView(R.id.console_flip); 675 TerminalBridge bridge = terminalView.bridge; 676 // TODO(raaar): Replace with process log. 677 VDUBuffer buffer = bridge.getVDUBuffer(); 678 int height = buffer.getRows(); 679 int width = buffer.getColumns(); 680 StringBuilder string = new StringBuilder(); 681 for (int i = 0; i < height; i++) { 682 for (int j = 0; j < width; j++) { 683 string.append(buffer.getChar(j, i)); 684 } 685 } 686 String addr = "user@example.com"; 687 Intent intent = new Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:" + addr)); 688 intent.putExtra("body", string.toString().trim()); 689 startActivity(intent); 690 } 691 692 @Override onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo)693 public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) { 694 TerminalBridge bridge = ((TerminalView) findCurrentView(R.id.console_flip)).bridge; 695 boolean sessionOpen = bridge.isSessionOpen(); 696 menu.add(Menu.NONE, MenuId.COPY.getId(), Menu.NONE, R.string.terminal_menu_copy); 697 if (clipboard.hasText() && sessionOpen) { 698 menu.add(Menu.NONE, MenuId.PASTE.getId(), Menu.NONE, R.string.terminal_menu_paste); 699 } 700 bridge.onCreateContextMenu(menu, view, menuInfo); 701 } 702 703 @Override onContextItemSelected(MenuItem item)704 public boolean onContextItemSelected(MenuItem item) { 705 int itemId = item.getItemId(); 706 if (itemId == MenuId.COPY.getId()) { 707 TerminalView terminalView = (TerminalView) findCurrentView(R.id.console_flip); 708 copySource = terminalView.bridge; 709 SelectionArea area = copySource.getSelectionArea(); 710 area.reset(); 711 area.setBounds(copySource.getVDUBuffer().getColumns(), copySource.getVDUBuffer().getRows()); 712 copySource.setSelectingForCopy(true); 713 // Make sure we show the initial selection 714 copySource.redraw(); 715 Toast.makeText(ConsoleActivity.this, getString(R.string.terminal_copy_start), 716 Toast.LENGTH_LONG).show(); 717 return true; 718 } else if (itemId == MenuId.PASTE.getId()) { 719 TerminalView terminalView = (TerminalView) findCurrentView(R.id.console_flip); 720 TerminalBridge bridge = terminalView.bridge; 721 // pull string from clipboard and generate all events to force down 722 String clip = clipboard.getText().toString(); 723 bridge.injectString(clip); 724 return true; 725 } 726 return false; 727 } 728 729 @Override onStart()730 public void onStart() { 731 super.onStart(); 732 // connect with manager service to find all bridges 733 // when connected it will insert all views 734 bindService(new Intent(this, ScriptingLayerService.class), mConnection, 0); 735 } 736 737 @Override onPause()738 public void onPause() { 739 super.onPause(); 740 Log.d("onPause called"); 741 742 // Allow the screen to dim and fall asleep. 743 if (wakelock != null && wakelock.isHeld()) { 744 wakelock.release(); 745 } 746 747 if (forcedOrientation && manager != null) { 748 manager.setResizeAllowed(false); 749 } 750 } 751 752 @Override onResume()753 public void onResume() { 754 super.onResume(); 755 Log.d("onResume called"); 756 757 // Make sure we don't let the screen fall asleep. 758 // This also keeps the Wi-Fi chipset from disconnecting us. 759 if (wakelock != null && prefs.getBoolean(PreferenceConstants.KEEP_ALIVE, true)) { 760 wakelock.acquire(); 761 } 762 763 configureOrientation(); 764 765 if (forcedOrientation && manager != null) { 766 manager.setResizeAllowed(true); 767 } 768 } 769 770 /* 771 * (non-Javadoc) 772 * 773 * @see android.app.Activity#onNewIntent(android.content.Intent) 774 */ 775 @Override onNewIntent(Intent intent)776 protected void onNewIntent(Intent intent) { 777 super.onNewIntent(intent); 778 779 Log.d("onNewIntent called"); 780 781 int id = intent.getIntExtra(Constants.EXTRA_PROXY_PORT, -1); 782 783 if (id > 0) { 784 processID = id; 785 } 786 787 if (processID == null) { 788 Log.e("Got null intent data in onNewIntent()"); 789 return; 790 } 791 792 if (manager == null) { 793 Log.e("We're not bound in onNewIntent()"); 794 return; 795 } 796 797 TerminalBridge requestedBridge = manager.getConnectedBridge(processID); 798 int requestedIndex = 0; 799 800 synchronized (flip) { 801 if (requestedBridge == null) { 802 // If we didn't find the requested connection, try opening it 803 804 try { 805 Log.d(String.format("We couldnt find an existing bridge with id = %d," 806 + "so creating one now", processID)); 807 requestedBridge = manager.openConnection(processID); 808 } catch (Exception e) { 809 Log.e("Problem while trying to create new requested bridge", e); 810 } 811 812 requestedIndex = addNewTerminalView(requestedBridge); 813 } else { 814 final int flipIndex = getFlipIndex(requestedBridge); 815 if (flipIndex > requestedIndex) { 816 requestedIndex = flipIndex; 817 } 818 } 819 820 setDisplayedTerminal(requestedIndex); 821 } 822 } 823 824 @Override onStop()825 public void onStop() { 826 super.onStop(); 827 unbindService(mConnection); 828 } 829 shiftCurrentTerminal(final int direction)830 protected void shiftCurrentTerminal(final int direction) { 831 View overlay; 832 synchronized (flip) { 833 boolean shouldAnimate = flip.getChildCount() > 1; 834 835 // Only show animation if there is something else to go to. 836 if (shouldAnimate) { 837 // keep current overlay from popping up again 838 overlay = findCurrentView(R.id.terminal_overlay); 839 if (overlay != null) { 840 overlay.startAnimation(fade_stay_hidden); 841 } 842 843 if (direction == SHIFT_LEFT) { 844 flip.setInAnimation(slide_left_in); 845 flip.setOutAnimation(slide_left_out); 846 flip.showNext(); 847 } else if (direction == SHIFT_RIGHT) { 848 flip.setInAnimation(slide_right_in); 849 flip.setOutAnimation(slide_right_out); 850 flip.showPrevious(); 851 } 852 } 853 854 if (shouldAnimate) { 855 // show overlay on new slide and start fade 856 overlay = findCurrentView(R.id.terminal_overlay); 857 if (overlay != null) { 858 overlay.startAnimation(fade_out_delayed); 859 } 860 } 861 862 updatePromptVisible(); 863 } 864 } 865 866 /** 867 * Show any prompts requested by the currently visible {@link TerminalView}. 868 */ updatePromptVisible()869 protected void updatePromptVisible() { 870 // check if our currently-visible terminalbridge is requesting any prompt services 871 View view = findCurrentView(R.id.console_flip); 872 873 // Hide all the prompts in case a prompt request was canceled 874 hideAllPrompts(); 875 876 if (!(view instanceof TerminalView)) { 877 // we dont have an active view, so hide any prompts 878 return; 879 } 880 881 PromptHelper prompt = ((TerminalView) view).bridge.getPromptHelper(); 882 883 if (Boolean.class.equals(prompt.promptRequested)) { 884 booleanPromptGroup.setVisibility(View.VISIBLE); 885 booleanPrompt.setText(prompt.promptHint); 886 booleanYes.requestFocus(); 887 } else { 888 hideAllPrompts(); 889 view.requestFocus(); 890 } 891 } 892 893 @Override onConfigurationChanged(Configuration newConfig)894 public void onConfigurationChanged(Configuration newConfig) { 895 super.onConfigurationChanged(newConfig); 896 897 Log.d(String.format( 898 "onConfigurationChanged; requestedOrientation=%d, newConfig.orientation=%d", 899 getRequestedOrientation(), newConfig.orientation)); 900 if (manager != null) { 901 if (forcedOrientation 902 && (newConfig.orientation != Configuration.ORIENTATION_LANDSCAPE && getRequestedOrientation() == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) 903 || (newConfig.orientation != Configuration.ORIENTATION_PORTRAIT && getRequestedOrientation() == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT)) { 904 manager.setResizeAllowed(false); 905 } else { 906 manager.setResizeAllowed(true); 907 } 908 909 manager 910 .setHardKeyboardHidden(newConfig.hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_YES); 911 } 912 } 913 914 /** 915 * Adds a new TerminalBridge to the current set of views in our ViewFlipper. 916 * 917 * @param bridge 918 * TerminalBridge to add to our ViewFlipper 919 * @return the child index of the new view in the ViewFlipper 920 */ addNewTerminalView(TerminalBridge bridge)921 private int addNewTerminalView(TerminalBridge bridge) { 922 // let them know about our prompt handler services 923 bridge.getPromptHelper().setHandler(promptHandler); 924 925 // inflate each terminal view 926 RelativeLayout view = (RelativeLayout) inflater.inflate(R.layout.item_terminal, flip, false); 927 928 // set the terminal overlay text 929 TextView overlay = (TextView) view.findViewById(R.id.terminal_overlay); 930 overlay.setText(bridge.getName()); 931 932 // and add our terminal view control, using index to place behind overlay 933 TerminalView terminal = new TerminalView(ConsoleActivity.this, bridge); 934 terminal.setId(R.id.console_flip); 935 view.addView(terminal, 0); 936 937 synchronized (flip) { 938 // finally attach to the flipper 939 flip.addView(view); 940 return flip.getChildCount() - 1; 941 } 942 } 943 getFlipIndex(TerminalBridge bridge)944 private int getFlipIndex(TerminalBridge bridge) { 945 synchronized (flip) { 946 final int children = flip.getChildCount(); 947 for (int i = 0; i < children; i++) { 948 final View view = flip.getChildAt(i).findViewById(R.id.console_flip); 949 950 if (view == null || !(view instanceof TerminalView)) { 951 // How did that happen? 952 continue; 953 } 954 955 final TerminalView tv = (TerminalView) view; 956 957 if (tv.bridge == bridge) { 958 return i; 959 } 960 } 961 } 962 963 return -1; 964 } 965 966 /** 967 * Displays the child in the ViewFlipper at the requestedIndex and updates the prompts. 968 * 969 * @param requestedIndex 970 * the index of the terminal view to display 971 */ setDisplayedTerminal(int requestedIndex)972 private void setDisplayedTerminal(int requestedIndex) { 973 synchronized (flip) { 974 try { 975 // show the requested bridge if found, also fade out overlay 976 flip.setDisplayedChild(requestedIndex); 977 flip.getCurrentView().findViewById(R.id.terminal_overlay).startAnimation(fade_out_delayed); 978 } catch (NullPointerException npe) { 979 Log.d("View went away when we were about to display it", npe); 980 } 981 updatePromptVisible(); 982 } 983 } 984 } 985