1 /* 2 * Copyright (C) 2007 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.term; 18 19 import java.io.FileDescriptor; 20 import java.io.FileInputStream; 21 import java.io.FileOutputStream; 22 import java.io.IOException; 23 import java.util.ArrayList; 24 25 import android.app.Activity; 26 import android.app.AlertDialog; 27 import android.content.Context; 28 import android.content.Intent; 29 import android.content.SharedPreferences; 30 import android.content.res.Configuration; 31 import android.content.res.Resources; 32 import android.content.res.TypedArray; 33 import android.graphics.Bitmap; 34 import android.graphics.BitmapFactory; 35 import android.graphics.Canvas; 36 import android.graphics.ColorMatrixColorFilter; 37 import android.graphics.Paint; 38 import android.graphics.PorterDuff; 39 import android.graphics.PorterDuffXfermode; 40 import android.graphics.Rect; 41 import android.graphics.Typeface; 42 import android.net.Uri; 43 import android.os.Bundle; 44 import android.os.Handler; 45 import android.os.Message; 46 import android.preference.PreferenceManager; 47 import android.util.AttributeSet; 48 import android.util.Log; 49 import android.view.GestureDetector; 50 import android.view.KeyEvent; 51 import android.view.Menu; 52 import android.view.MenuItem; 53 import android.view.MotionEvent; 54 import android.view.View; 55 import android.view.inputmethod.BaseInputConnection; 56 import android.view.inputmethod.CompletionInfo; 57 import android.view.inputmethod.EditorInfo; 58 import android.view.inputmethod.ExtractedText; 59 import android.view.inputmethod.ExtractedTextRequest; 60 import android.view.inputmethod.InputConnection; 61 62 /** 63 * A terminal emulator activity. 64 */ 65 66 public class Term extends Activity { 67 /** 68 * Set to true to add debugging code and logging. 69 */ 70 public static final boolean DEBUG = false; 71 72 /** 73 * Set to true to log each character received from the remote process to the 74 * android log, which makes it easier to debug some kinds of problems with 75 * emulating escape sequences and control codes. 76 */ 77 public static final boolean LOG_CHARACTERS_FLAG = DEBUG && false; 78 79 /** 80 * Set to true to log unknown escape sequences. 81 */ 82 public static final boolean LOG_UNKNOWN_ESCAPE_SEQUENCES = DEBUG && false; 83 84 /** 85 * The tag we use when logging, so that our messages can be distinguished 86 * from other messages in the log. Public because it's used by several 87 * classes. 88 */ 89 public static final String LOG_TAG = "Term"; 90 91 /** 92 * Our main view. Displays the emulated terminal screen. 93 */ 94 private EmulatorView mEmulatorView; 95 96 /** 97 * The pseudo-teletype (pty) file descriptor that we use to communicate with 98 * another process, typically a shell. 99 */ 100 private FileDescriptor mTermFd; 101 102 /** 103 * Used to send data to the remote process. 104 */ 105 private FileOutputStream mTermOut; 106 107 /** 108 * A key listener that tracks the modifier keys and allows the full ASCII 109 * character set to be entered. 110 */ 111 private TermKeyListener mKeyListener; 112 113 /** 114 * The name of our emulator view in the view resource. 115 */ 116 private static final int EMULATOR_VIEW = R.id.emulatorView; 117 118 private int mFontSize = 9; 119 private int mColorId = 2; 120 private int mControlKeyId = 0; 121 122 private static final String FONTSIZE_KEY = "fontsize"; 123 private static final String COLOR_KEY = "color"; 124 private static final String CONTROLKEY_KEY = "controlkey"; 125 private static final String SHELL_KEY = "shell"; 126 private static final String INITIALCOMMAND_KEY = "initialcommand"; 127 128 public static final int WHITE = 0xffffffff; 129 public static final int BLACK = 0xff000000; 130 public static final int BLUE = 0xff344ebd; 131 132 private static final int[][] COLOR_SCHEMES = { 133 {BLACK, WHITE}, {WHITE, BLACK}, {WHITE, BLUE}}; 134 135 private static final int[] CONTROL_KEY_SCHEMES = { 136 KeyEvent.KEYCODE_DPAD_CENTER, 137 KeyEvent.KEYCODE_AT, 138 KeyEvent.KEYCODE_ALT_LEFT, 139 KeyEvent.KEYCODE_ALT_RIGHT 140 }; 141 private static final String[] CONTROL_KEY_NAME = { 142 "Ball", "@", "Left-Alt", "Right-Alt" 143 }; 144 145 private int mControlKeyCode; 146 147 private final static String DEFAULT_SHELL = "/system/bin/sh -"; 148 private String mShell; 149 150 private final static String DEFAULT_INITIAL_COMMAND = 151 "export PATH=/data/local/bin:$PATH"; 152 private String mInitialCommand; 153 154 private SharedPreferences mPrefs; 155 156 @Override onCreate(Bundle icicle)157 public void onCreate(Bundle icicle) { 158 super.onCreate(icicle); 159 Log.e(Term.LOG_TAG, "onCreate"); 160 mPrefs = PreferenceManager.getDefaultSharedPreferences(this); 161 mPrefs.registerOnSharedPreferenceChangeListener( 162 new SharedPreferences.OnSharedPreferenceChangeListener(){ 163 164 public void onSharedPreferenceChanged( 165 SharedPreferences sharedPreferences, String key) { 166 readPrefs(); 167 updatePrefs(); 168 }}); 169 readPrefs(); 170 171 setContentView(R.layout.term_activity); 172 173 mEmulatorView = (EmulatorView) findViewById(EMULATOR_VIEW); 174 175 startListening(); 176 177 mKeyListener = new TermKeyListener(); 178 179 mEmulatorView.setFocusable(true); 180 mEmulatorView.setFocusableInTouchMode(true); 181 mEmulatorView.requestFocus(); 182 mEmulatorView.register(mKeyListener); 183 184 updatePrefs(); 185 } 186 187 @Override onDestroy()188 public void onDestroy() { 189 super.onDestroy(); 190 if (mTermFd != null) { 191 Exec.close(mTermFd); 192 mTermFd = null; 193 } 194 } 195 startListening()196 private void startListening() { 197 int[] processId = new int[1]; 198 199 createSubprocess(processId); 200 final int procId = processId[0]; 201 202 final Handler handler = new Handler() { 203 @Override 204 public void handleMessage(Message msg) { 205 } 206 }; 207 208 Runnable watchForDeath = new Runnable() { 209 210 public void run() { 211 Log.i(Term.LOG_TAG, "waiting for: " + procId); 212 int result = Exec.waitFor(procId); 213 Log.i(Term.LOG_TAG, "Subprocess exited: " + result); 214 handler.sendEmptyMessage(result); 215 } 216 217 }; 218 Thread watcher = new Thread(watchForDeath); 219 watcher.start(); 220 221 mTermOut = new FileOutputStream(mTermFd); 222 223 mEmulatorView.initialize(mTermFd, mTermOut); 224 225 sendInitialCommand(); 226 } 227 sendInitialCommand()228 private void sendInitialCommand() { 229 String initialCommand = mInitialCommand; 230 if (initialCommand == null) { 231 initialCommand = DEFAULT_INITIAL_COMMAND; 232 } 233 if (initialCommand.length() > 0) { 234 write(initialCommand + '\r'); 235 } 236 } 237 restart()238 private void restart() { 239 startActivity(getIntent()); 240 finish(); 241 } 242 write(String data)243 private void write(String data) { 244 try { 245 mTermOut.write(data.getBytes()); 246 mTermOut.flush(); 247 } catch (IOException e) { 248 // Ignore exception 249 // We don't really care if the receiver isn't listening. 250 // We just make a best effort to answer the query. 251 } 252 } 253 createSubprocess(int[] processId)254 private void createSubprocess(int[] processId) { 255 String shell = mShell; 256 if (shell == null) { 257 shell = DEFAULT_SHELL; 258 } 259 ArrayList<String> args = parse(shell); 260 String arg0 = args.get(0); 261 String arg1 = null; 262 String arg2 = null; 263 if (args.size() >= 2) { 264 arg1 = args.get(1); 265 } 266 if (args.size() >= 3) { 267 arg2 = args.get(2); 268 } 269 mTermFd = Exec.createSubprocess(arg0, arg1, arg2, processId); 270 } 271 parse(String cmd)272 private ArrayList<String> parse(String cmd) { 273 final int PLAIN = 0; 274 final int WHITESPACE = 1; 275 final int INQUOTE = 2; 276 int state = WHITESPACE; 277 ArrayList<String> result = new ArrayList<String>(); 278 int cmdLen = cmd.length(); 279 StringBuilder builder = new StringBuilder(); 280 for (int i = 0; i < cmdLen; i++) { 281 char c = cmd.charAt(i); 282 if (state == PLAIN) { 283 if (Character.isWhitespace(c)) { 284 result.add(builder.toString()); 285 builder.delete(0,builder.length()); 286 state = WHITESPACE; 287 } else if (c == '"') { 288 state = INQUOTE; 289 } else { 290 builder.append(c); 291 } 292 } else if (state == WHITESPACE) { 293 if (Character.isWhitespace(c)) { 294 // do nothing 295 } else if (c == '"') { 296 state = INQUOTE; 297 } else { 298 state = PLAIN; 299 builder.append(c); 300 } 301 } else if (state == INQUOTE) { 302 if (c == '\\') { 303 if (i + 1 < cmdLen) { 304 i += 1; 305 builder.append(cmd.charAt(i)); 306 } 307 } else if (c == '"') { 308 state = PLAIN; 309 } else { 310 builder.append(c); 311 } 312 } 313 } 314 if (builder.length() > 0) { 315 result.add(builder.toString()); 316 } 317 return result; 318 } 319 readPrefs()320 private void readPrefs() { 321 mFontSize = readIntPref(FONTSIZE_KEY, mFontSize, 20); 322 mColorId = readIntPref(COLOR_KEY, mColorId, COLOR_SCHEMES.length - 1); 323 mControlKeyId = readIntPref(CONTROLKEY_KEY, mControlKeyId, 324 CONTROL_KEY_SCHEMES.length - 1); 325 { 326 String newShell = readStringPref(SHELL_KEY, mShell); 327 if ((newShell == null) || ! newShell.equals(mShell)) { 328 if (mShell != null) { 329 Log.i(Term.LOG_TAG, "New shell set. Restarting."); 330 restart(); 331 } 332 mShell = newShell; 333 } 334 } 335 { 336 String newInitialCommand = readStringPref(INITIALCOMMAND_KEY, 337 mInitialCommand); 338 if ((newInitialCommand == null) 339 || ! newInitialCommand.equals(mInitialCommand)) { 340 if (mInitialCommand != null) { 341 Log.i(Term.LOG_TAG, "New initial command set. Restarting."); 342 restart(); 343 } 344 mInitialCommand = newInitialCommand; 345 } 346 } 347 } 348 updatePrefs()349 private void updatePrefs() { 350 mEmulatorView.setTextSize(mFontSize); 351 setColors(); 352 mControlKeyCode = CONTROL_KEY_SCHEMES[mControlKeyId]; 353 } 354 readIntPref(String key, int defaultValue, int maxValue)355 private int readIntPref(String key, int defaultValue, int maxValue) { 356 int val; 357 try { 358 val = Integer.parseInt( 359 mPrefs.getString(key, Integer.toString(defaultValue))); 360 } catch (NumberFormatException e) { 361 val = defaultValue; 362 } 363 val = Math.max(0, Math.min(val, maxValue)); 364 return val; 365 } 366 readStringPref(String key, String defaultValue)367 private String readStringPref(String key, String defaultValue) { 368 return mPrefs.getString(key, defaultValue); 369 } 370 371 @Override onPause()372 public void onPause() { 373 SharedPreferences.Editor e = mPrefs.edit(); 374 e.clear(); 375 e.putString(FONTSIZE_KEY, Integer.toString(mFontSize)); 376 e.putString(COLOR_KEY, Integer.toString(mColorId)); 377 e.putString(CONTROLKEY_KEY, Integer.toString(mControlKeyId)); 378 e.putString(SHELL_KEY, mShell); 379 e.putString(INITIALCOMMAND_KEY, mInitialCommand); 380 e.commit(); 381 382 super.onPause(); 383 } 384 385 @Override onConfigurationChanged(Configuration newConfig)386 public void onConfigurationChanged(Configuration newConfig) { 387 super.onConfigurationChanged(newConfig); 388 389 mEmulatorView.updateSize(); 390 } 391 392 @Override onKeyDown(int keyCode, KeyEvent event)393 public boolean onKeyDown(int keyCode, KeyEvent event) { 394 if (handleControlKey(keyCode, true)) { 395 return true; 396 } else if (isSystemKey(keyCode, event)) { 397 // Don't intercept the system keys 398 return super.onKeyDown(keyCode, event); 399 } else if (handleDPad(keyCode, true)) { 400 return true; 401 } 402 403 // Translate the keyCode into an ASCII character. 404 int letter = mKeyListener.keyDown(keyCode, event); 405 406 if (letter >= 0) { 407 try { 408 mTermOut.write(letter); 409 } catch (IOException e) { 410 // Ignore I/O exceptions 411 } 412 } 413 return true; 414 } 415 416 @Override onKeyUp(int keyCode, KeyEvent event)417 public boolean onKeyUp(int keyCode, KeyEvent event) { 418 if (handleControlKey(keyCode, false)) { 419 return true; 420 } else if (isSystemKey(keyCode, event)) { 421 // Don't intercept the system keys 422 return super.onKeyUp(keyCode, event); 423 } else if (handleDPad(keyCode, false)) { 424 return true; 425 } 426 427 mKeyListener.keyUp(keyCode); 428 return true; 429 } 430 handleControlKey(int keyCode, boolean down)431 private boolean handleControlKey(int keyCode, boolean down) { 432 if (keyCode == mControlKeyCode) { 433 mKeyListener.handleControlKey(down); 434 return true; 435 } 436 return false; 437 } 438 439 /** 440 * Handle dpad left-right-up-down events. Don't handle 441 * dpad-center, that's our control key. 442 * @param keyCode 443 * @param down 444 */ handleDPad(int keyCode, boolean down)445 private boolean handleDPad(int keyCode, boolean down) { 446 if (keyCode < KeyEvent.KEYCODE_DPAD_UP || 447 keyCode > KeyEvent.KEYCODE_DPAD_CENTER) { 448 return false; 449 } 450 451 if (down) { 452 try { 453 if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) { 454 mTermOut.write('\r'); 455 } else { 456 char code; 457 switch (keyCode) { 458 case KeyEvent.KEYCODE_DPAD_UP: 459 code = 'A'; 460 break; 461 case KeyEvent.KEYCODE_DPAD_DOWN: 462 code = 'B'; 463 break; 464 case KeyEvent.KEYCODE_DPAD_LEFT: 465 code = 'D'; 466 break; 467 default: 468 case KeyEvent.KEYCODE_DPAD_RIGHT: 469 code = 'C'; 470 break; 471 } 472 mTermOut.write(27); // ESC 473 if (mEmulatorView.getKeypadApplicationMode()) { 474 mTermOut.write('O'); 475 } else { 476 mTermOut.write('['); 477 } 478 mTermOut.write(code); 479 } 480 } catch (IOException e) { 481 // Ignore 482 } 483 } 484 return true; 485 } 486 isSystemKey(int keyCode, KeyEvent event)487 private boolean isSystemKey(int keyCode, KeyEvent event) { 488 return event.isSystem(); 489 } 490 491 @Override onCreateOptionsMenu(Menu menu)492 public boolean onCreateOptionsMenu(Menu menu) { 493 getMenuInflater().inflate(R.menu.main, menu); 494 return true; 495 } 496 497 @Override onOptionsItemSelected(MenuItem item)498 public boolean onOptionsItemSelected(MenuItem item) { 499 int id = item.getItemId(); 500 if (id == R.id.menu_preferences) { 501 doPreferences(); 502 } else if (id == R.id.menu_reset) { 503 doResetTerminal(); 504 } else if (id == R.id.menu_send_email) { 505 doEmailTranscript(); 506 } else if (id == R.id.menu_special_keys) { 507 doDocumentKeys(); 508 } 509 return super.onOptionsItemSelected(item); 510 } 511 doPreferences()512 private void doPreferences() { 513 startActivity(new Intent(this, TermPreferences.class)); 514 } 515 setColors()516 private void setColors() { 517 int[] scheme = COLOR_SCHEMES[mColorId]; 518 mEmulatorView.setColors(scheme[0], scheme[1]); 519 } 520 doResetTerminal()521 private void doResetTerminal() { 522 restart(); 523 } 524 doEmailTranscript()525 private void doEmailTranscript() { 526 // Don't really want to supply an address, but 527 // currently it's required, otherwise we get an 528 // exception. 529 String addr = "user@example.com"; 530 Intent intent = 531 new Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:" 532 + addr)); 533 534 intent.putExtra("body", mEmulatorView.getTranscriptText()); 535 startActivity(intent); 536 } 537 doDocumentKeys()538 private void doDocumentKeys() { 539 String controlKey = CONTROL_KEY_NAME[mControlKeyId]; 540 new AlertDialog.Builder(this). 541 setTitle("Press " + controlKey + " and Key"). 542 setMessage(controlKey + " Space ==> Control-@ (NUL)\n" 543 + controlKey + " A..Z ==> Control-A..Z\n" 544 + controlKey + " 1 ==> Control-[ (ESC)\n" 545 + controlKey + " 5 ==> Control-_\n" 546 + controlKey + " . ==> Control-\\\n" 547 + controlKey + " 0 ==> Control-]\n" 548 + controlKey + " 6 ==> Control-^"). 549 show(); 550 } 551 } 552 553 554 /** 555 * An abstract screen interface. A terminal screen stores lines of text. (The 556 * reason to abstract it is to allow different implementations, and to hide 557 * implementation details from clients.) 558 */ 559 interface Screen { 560 561 /** 562 * Set line wrap flag for a given row. Affects how lines are logically 563 * wrapped when changing screen size or converting to a transcript. 564 */ setLineWrap(int row)565 void setLineWrap(int row); 566 567 /** 568 * Store byte b into the screen at location (x, y) 569 * 570 * @param x X coordinate (also known as column) 571 * @param y Y coordinate (also known as row) 572 * @param b ASCII character to store 573 * @param foreColor the foreground color 574 * @param backColor the background color 575 */ set(int x, int y, byte b, int foreColor, int backColor)576 void set(int x, int y, byte b, int foreColor, int backColor); 577 578 /** 579 * Scroll the screen down one line. To scroll the whole screen of a 24 line 580 * screen, the arguments would be (0, 24). 581 * 582 * @param topMargin First line that is scrolled. 583 * @param bottomMargin One line after the last line that is scrolled. 584 */ scroll(int topMargin, int bottomMargin, int foreColor, int backColor)585 void scroll(int topMargin, int bottomMargin, int foreColor, int backColor); 586 587 /** 588 * Block copy characters from one position in the screen to another. The two 589 * positions can overlap. All characters of the source and destination must 590 * be within the bounds of the screen, or else an InvalidParemeterException 591 * will be thrown. 592 * 593 * @param sx source X coordinate 594 * @param sy source Y coordinate 595 * @param w width 596 * @param h height 597 * @param dx destination X coordinate 598 * @param dy destination Y coordinate 599 */ blockCopy(int sx, int sy, int w, int h, int dx, int dy)600 void blockCopy(int sx, int sy, int w, int h, int dx, int dy); 601 602 /** 603 * Block set characters. All characters must be within the bounds of the 604 * screen, or else and InvalidParemeterException will be thrown. Typically 605 * this is called with a "val" argument of 32 to clear a block of 606 * characters. 607 * 608 * @param sx source X 609 * @param sy source Y 610 * @param w width 611 * @param h height 612 * @param val value to set. 613 * @param foreColor the foreground color 614 * @param backColor the background color 615 */ blockSet(int sx, int sy, int w, int h, int val, int foreColor, int backColor)616 void blockSet(int sx, int sy, int w, int h, int val, int foreColor, int 617 backColor); 618 619 /** 620 * Get the contents of the transcript buffer as a text string. 621 * 622 * @return the contents of the transcript buffer. 623 */ getTranscriptText()624 String getTranscriptText(); 625 626 /** 627 * Resize the screen 628 * @param columns 629 * @param rows 630 */ resize(int columns, int rows, int foreColor, int backColor)631 void resize(int columns, int rows, int foreColor, int backColor); 632 } 633 634 635 /** 636 * A TranscriptScreen is a screen that remembers data that's been scrolled. The 637 * old data is stored in a ring buffer to minimize the amount of copying that 638 * needs to be done. The transcript does its own drawing, to avoid having to 639 * expose its internal data structures. 640 */ 641 class TranscriptScreen implements Screen { 642 643 /** 644 * The width of the transcript, in characters. Fixed at initialization. 645 */ 646 private int mColumns; 647 648 /** 649 * The total number of rows in the transcript and the screen. Fixed at 650 * initialization. 651 */ 652 private int mTotalRows; 653 654 /** 655 * The number of rows in the active portion of the transcript. Doesn't 656 * include the screen. 657 */ 658 private int mActiveTranscriptRows; 659 660 /** 661 * Which row is currently the topmost line of the transcript. Used to 662 * implement a circular buffer. 663 */ 664 private int mHead; 665 666 /** 667 * The number of active rows, includes both the transcript and the screen. 668 */ 669 private int mActiveRows; 670 671 /** 672 * The number of rows in the screen. 673 */ 674 private int mScreenRows; 675 676 /** 677 * The data for both the screen and the transcript. The first mScreenRows * 678 * mLineWidth characters are the screen, the rest are the transcript. 679 * The low byte encodes the ASCII character, the high byte encodes the 680 * foreground and background colors, plus underline and bold. 681 */ 682 private char[] mData; 683 684 /** 685 * The data's stored as color-encoded chars, but the drawing routines require chars, so we 686 * need a temporary buffer to hold a row's worth of characters. 687 */ 688 private char[] mRowBuffer; 689 690 /** 691 * Flags that keep track of whether the current line logically wraps to the 692 * next line. This is used when resizing the screen and when copying to the 693 * clipboard or an email attachment 694 */ 695 696 private boolean[] mLineWrap; 697 698 /** 699 * Create a transcript screen. 700 * 701 * @param columns the width of the screen in characters. 702 * @param totalRows the height of the entire text area, in rows of text. 703 * @param screenRows the height of just the screen, not including the 704 * transcript that holds lines that have scrolled off the top of the 705 * screen. 706 */ TranscriptScreen(int columns, int totalRows, int screenRows, int foreColor, int backColor)707 public TranscriptScreen(int columns, int totalRows, int screenRows, 708 int foreColor, int backColor) { 709 init(columns, totalRows, screenRows, foreColor, backColor); 710 } 711 init(int columns, int totalRows, int screenRows, int foreColor, int backColor)712 private void init(int columns, int totalRows, int screenRows, int foreColor, int backColor) { 713 mColumns = columns; 714 mTotalRows = totalRows; 715 mActiveTranscriptRows = 0; 716 mHead = 0; 717 mActiveRows = screenRows; 718 mScreenRows = screenRows; 719 int totalSize = columns * totalRows; 720 mData = new char[totalSize]; 721 blockSet(0, 0, mColumns, mScreenRows, ' ', foreColor, backColor); 722 mRowBuffer = new char[columns]; 723 mLineWrap = new boolean[totalRows]; 724 consistencyCheck(); 725 } 726 727 /** 728 * Convert a row value from the public external coordinate system to our 729 * internal private coordinate system. External coordinate system: 730 * -mActiveTranscriptRows to mScreenRows-1, with the screen being 731 * 0..mScreenRows-1 Internal coordinate system: 0..mScreenRows-1 rows of 732 * mData are the visible rows. mScreenRows..mActiveRows - 1 are the 733 * transcript, stored as a circular buffer. 734 * 735 * @param row a row in the external coordinate system. 736 * @return The row corresponding to the input argument in the private 737 * coordinate system. 738 */ externalToInternalRow(int row)739 private int externalToInternalRow(int row) { 740 if (row < -mActiveTranscriptRows || row >= mScreenRows) { 741 throw new IllegalArgumentException(); 742 } 743 if (row >= 0) { 744 return row; // This is a visible row. 745 } 746 return mScreenRows 747 + ((mHead + mActiveTranscriptRows + row) % mActiveTranscriptRows); 748 } 749 getOffset(int externalLine)750 private int getOffset(int externalLine) { 751 return externalToInternalRow(externalLine) * mColumns; 752 } 753 getOffset(int x, int y)754 private int getOffset(int x, int y) { 755 return getOffset(y) + x; 756 } 757 setLineWrap(int row)758 public void setLineWrap(int row) { 759 mLineWrap[externalToInternalRow(row)] = true; 760 } 761 762 /** 763 * Store byte b into the screen at location (x, y) 764 * 765 * @param x X coordinate (also known as column) 766 * @param y Y coordinate (also known as row) 767 * @param b ASCII character to store 768 * @param foreColor the foreground color 769 * @param backColor the background color 770 */ set(int x, int y, byte b, int foreColor, int backColor)771 public void set(int x, int y, byte b, int foreColor, int backColor) { 772 mData[getOffset(x, y)] = encode(b, foreColor, backColor); 773 } 774 encode(int b, int foreColor, int backColor)775 private char encode(int b, int foreColor, int backColor) { 776 return (char) ((foreColor << 12) | (backColor << 8) | b); 777 } 778 779 /** 780 * Scroll the screen down one line. To scroll the whole screen of a 24 line 781 * screen, the arguments would be (0, 24). 782 * 783 * @param topMargin First line that is scrolled. 784 * @param bottomMargin One line after the last line that is scrolled. 785 */ scroll(int topMargin, int bottomMargin, int foreColor, int backColor)786 public void scroll(int topMargin, int bottomMargin, int foreColor, 787 int backColor) { 788 if (topMargin > bottomMargin - 2 || topMargin > mScreenRows - 2 789 || bottomMargin > mScreenRows) { 790 throw new IllegalArgumentException(); 791 } 792 793 // Adjust the transcript so that the last line of the transcript 794 // is ready to receive the newly scrolled data 795 consistencyCheck(); 796 int expansionRows = Math.min(1, mTotalRows - mActiveRows); 797 int rollRows = 1 - expansionRows; 798 mActiveRows += expansionRows; 799 mActiveTranscriptRows += expansionRows; 800 if (mActiveTranscriptRows > 0) { 801 mHead = (mHead + rollRows) % mActiveTranscriptRows; 802 } 803 consistencyCheck(); 804 805 // Block move the scroll line to the transcript 806 int topOffset = getOffset(topMargin); 807 int destOffset = getOffset(-1); 808 System.arraycopy(mData, topOffset, mData, destOffset, mColumns); 809 810 int topLine = externalToInternalRow(topMargin); 811 int destLine = externalToInternalRow(-1); 812 System.arraycopy(mLineWrap, topLine, mLineWrap, destLine, 1); 813 814 // Block move the scrolled data up 815 int numScrollChars = (bottomMargin - topMargin - 1) * mColumns; 816 System.arraycopy(mData, topOffset + mColumns, mData, topOffset, 817 numScrollChars); 818 int numScrollLines = (bottomMargin - topMargin - 1); 819 System.arraycopy(mLineWrap, topLine + 1, mLineWrap, topLine, 820 numScrollLines); 821 822 // Erase the bottom line of the scroll region 823 blockSet(0, bottomMargin - 1, mColumns, 1, ' ', foreColor, backColor); 824 mLineWrap[externalToInternalRow(bottomMargin-1)] = false; 825 } 826 consistencyCheck()827 private void consistencyCheck() { 828 checkPositive(mColumns); 829 checkPositive(mTotalRows); 830 checkRange(0, mActiveTranscriptRows, mTotalRows); 831 if (mActiveTranscriptRows == 0) { 832 checkEqual(mHead, 0); 833 } else { 834 checkRange(0, mHead, mActiveTranscriptRows-1); 835 } 836 checkEqual(mScreenRows + mActiveTranscriptRows, mActiveRows); 837 checkRange(0, mScreenRows, mTotalRows); 838 839 checkEqual(mTotalRows, mLineWrap.length); 840 checkEqual(mTotalRows*mColumns, mData.length); 841 checkEqual(mColumns, mRowBuffer.length); 842 } 843 checkPositive(int n)844 private void checkPositive(int n) { 845 if (n < 0) { 846 throw new IllegalArgumentException("checkPositive " + n); 847 } 848 } 849 checkRange(int a, int b, int c)850 private void checkRange(int a, int b, int c) { 851 if (a > b || b > c) { 852 throw new IllegalArgumentException("checkRange " + a + " <= " + b + " <= " + c); 853 } 854 } 855 checkEqual(int a, int b)856 private void checkEqual(int a, int b) { 857 if (a != b) { 858 throw new IllegalArgumentException("checkEqual " + a + " == " + b); 859 } 860 } 861 862 /** 863 * Block copy characters from one position in the screen to another. The two 864 * positions can overlap. All characters of the source and destination must 865 * be within the bounds of the screen, or else an InvalidParemeterException 866 * will be thrown. 867 * 868 * @param sx source X coordinate 869 * @param sy source Y coordinate 870 * @param w width 871 * @param h height 872 * @param dx destination X coordinate 873 * @param dy destination Y coordinate 874 */ blockCopy(int sx, int sy, int w, int h, int dx, int dy)875 public void blockCopy(int sx, int sy, int w, int h, int dx, int dy) { 876 if (sx < 0 || sx + w > mColumns || sy < 0 || sy + h > mScreenRows 877 || dx < 0 || dx + w > mColumns || dy < 0 878 || dy + h > mScreenRows) { 879 throw new IllegalArgumentException(); 880 } 881 if (sy <= dy) { 882 // Move in increasing order 883 for (int y = 0; y < h; y++) { 884 int srcOffset = getOffset(sx, sy + y); 885 int dstOffset = getOffset(dx, dy + y); 886 System.arraycopy(mData, srcOffset, mData, dstOffset, w); 887 } 888 } else { 889 // Move in decreasing order 890 for (int y = 0; y < h; y++) { 891 int y2 = h - (y + 1); 892 int srcOffset = getOffset(sx, sy + y2); 893 int dstOffset = getOffset(dx, dy + y2); 894 System.arraycopy(mData, srcOffset, mData, dstOffset, w); 895 } 896 } 897 } 898 899 /** 900 * Block set characters. All characters must be within the bounds of the 901 * screen, or else and InvalidParemeterException will be thrown. Typically 902 * this is called with a "val" argument of 32 to clear a block of 903 * characters. 904 * 905 * @param sx source X 906 * @param sy source Y 907 * @param w width 908 * @param h height 909 * @param val value to set. 910 */ blockSet(int sx, int sy, int w, int h, int val, int foreColor, int backColor)911 public void blockSet(int sx, int sy, int w, int h, int val, 912 int foreColor, int backColor) { 913 if (sx < 0 || sx + w > mColumns || sy < 0 || sy + h > mScreenRows) { 914 throw new IllegalArgumentException(); 915 } 916 char[] data = mData; 917 char encodedVal = encode(val, foreColor, backColor); 918 for (int y = 0; y < h; y++) { 919 int offset = getOffset(sx, sy + y); 920 for (int x = 0; x < w; x++) { 921 data[offset + x] = encodedVal; 922 } 923 } 924 } 925 926 /** 927 * Draw a row of text. Out-of-bounds rows are blank, not errors. 928 * 929 * @param row The row of text to draw. 930 * @param canvas The canvas to draw to. 931 * @param x The x coordinate origin of the drawing 932 * @param y The y coordinate origin of the drawing 933 * @param renderer The renderer to use to draw the text 934 * @param cx the cursor X coordinate, -1 means don't draw it 935 */ drawText(int row, Canvas canvas, float x, float y, TextRenderer renderer, int cx)936 public final void drawText(int row, Canvas canvas, float x, float y, 937 TextRenderer renderer, int cx) { 938 939 // Out-of-bounds rows are blank. 940 if (row < -mActiveTranscriptRows || row >= mScreenRows) { 941 return; 942 } 943 944 // Copy the data from the byte array to a char array so they can 945 // be drawn. 946 947 int offset = getOffset(row); 948 char[] rowBuffer = mRowBuffer; 949 char[] data = mData; 950 int columns = mColumns; 951 int lastColors = 0; 952 int lastRunStart = -1; 953 final int CURSOR_MASK = 0x10000; 954 for (int i = 0; i < columns; i++) { 955 char c = data[offset + i]; 956 int colors = (char) (c & 0xff00); 957 if (cx == i) { 958 // Set cursor background color: 959 colors |= CURSOR_MASK; 960 } 961 rowBuffer[i] = (char) (c & 0x00ff); 962 if (colors != lastColors) { 963 if (lastRunStart >= 0) { 964 renderer.drawTextRun(canvas, x, y, lastRunStart, rowBuffer, 965 lastRunStart, i - lastRunStart, 966 (lastColors & CURSOR_MASK) != 0, 967 0xf & (lastColors >> 12), 0xf & (lastColors >> 8)); 968 } 969 lastColors = colors; 970 lastRunStart = i; 971 } 972 } 973 if (lastRunStart >= 0) { 974 renderer.drawTextRun(canvas, x, y, lastRunStart, rowBuffer, 975 lastRunStart, columns - lastRunStart, 976 (lastColors & CURSOR_MASK) != 0, 977 0xf & (lastColors >> 12), 0xf & (lastColors >> 8)); 978 } 979 } 980 981 /** 982 * Get the count of active rows. 983 * 984 * @return the count of active rows. 985 */ getActiveRows()986 public int getActiveRows() { 987 return mActiveRows; 988 } 989 990 /** 991 * Get the count of active transcript rows. 992 * 993 * @return the count of active transcript rows. 994 */ getActiveTranscriptRows()995 public int getActiveTranscriptRows() { 996 return mActiveTranscriptRows; 997 } 998 getTranscriptText()999 public String getTranscriptText() { 1000 return internalGetTranscriptText(true); 1001 } 1002 internalGetTranscriptText(boolean stripColors)1003 private String internalGetTranscriptText(boolean stripColors) { 1004 StringBuilder builder = new StringBuilder(); 1005 char[] rowBuffer = mRowBuffer; 1006 char[] data = mData; 1007 int columns = mColumns; 1008 for (int row = -mActiveTranscriptRows; row < mScreenRows; row++) { 1009 int offset = getOffset(row); 1010 int lastPrintingChar = -1; 1011 for (int column = 0; column < columns; column++) { 1012 char c = data[offset + column]; 1013 if (stripColors) { 1014 c = (char) (c & 0xff); 1015 } 1016 if ((c & 0xff) != ' ') { 1017 lastPrintingChar = column; 1018 } 1019 rowBuffer[column] = c; 1020 } 1021 if (mLineWrap[externalToInternalRow(row)]) { 1022 builder.append(rowBuffer, 0, columns); 1023 } else { 1024 builder.append(rowBuffer, 0, lastPrintingChar + 1); 1025 builder.append('\n'); 1026 } 1027 } 1028 return builder.toString(); 1029 } 1030 resize(int columns, int rows, int foreColor, int backColor)1031 public void resize(int columns, int rows, int foreColor, int backColor) { 1032 init(columns, mTotalRows, rows, foreColor, backColor); 1033 } 1034 } 1035 1036 /** 1037 * Renders text into a screen. Contains all the terminal-specific knowlege and 1038 * state. Emulates a subset of the X Window System xterm terminal, which in turn 1039 * is an emulator for a subset of the Digital Equipment Corporation vt100 1040 * terminal. Missing functionality: text attributes (bold, underline, reverse 1041 * video, color) alternate screen cursor key and keypad escape sequences. 1042 */ 1043 class TerminalEmulator { 1044 1045 /** 1046 * The cursor row. Numbered 0..mRows-1. 1047 */ 1048 private int mCursorRow; 1049 1050 /** 1051 * The cursor column. Numbered 0..mColumns-1. 1052 */ 1053 private int mCursorCol; 1054 1055 /** 1056 * The number of character rows in the terminal screen. 1057 */ 1058 private int mRows; 1059 1060 /** 1061 * The number of character columns in the terminal screen. 1062 */ 1063 private int mColumns; 1064 1065 /** 1066 * Used to send data to the remote process. Needed to implement the various 1067 * "report" escape sequences. 1068 */ 1069 private FileOutputStream mTermOut; 1070 1071 /** 1072 * Stores the characters that appear on the screen of the emulated terminal. 1073 */ 1074 private Screen mScreen; 1075 1076 /** 1077 * Keeps track of the current argument of the current escape sequence. 1078 * Ranges from 0 to MAX_ESCAPE_PARAMETERS-1. (Typically just 0 or 1.) 1079 */ 1080 private int mArgIndex; 1081 1082 /** 1083 * The number of parameter arguments. This name comes from the ANSI standard 1084 * for terminal escape codes. 1085 */ 1086 private static final int MAX_ESCAPE_PARAMETERS = 16; 1087 1088 /** 1089 * Holds the arguments of the current escape sequence. 1090 */ 1091 private int[] mArgs = new int[MAX_ESCAPE_PARAMETERS]; 1092 1093 // Escape processing states: 1094 1095 /** 1096 * Escape processing state: Not currently in an escape sequence. 1097 */ 1098 private static final int ESC_NONE = 0; 1099 1100 /** 1101 * Escape processing state: Have seen an ESC character 1102 */ 1103 private static final int ESC = 1; 1104 1105 /** 1106 * Escape processing state: Have seen ESC POUND 1107 */ 1108 private static final int ESC_POUND = 2; 1109 1110 /** 1111 * Escape processing state: Have seen ESC and a character-set-select char 1112 */ 1113 private static final int ESC_SELECT_LEFT_PAREN = 3; 1114 1115 /** 1116 * Escape processing state: Have seen ESC and a character-set-select char 1117 */ 1118 private static final int ESC_SELECT_RIGHT_PAREN = 4; 1119 1120 /** 1121 * Escape processing state: ESC [ 1122 */ 1123 private static final int ESC_LEFT_SQUARE_BRACKET = 5; 1124 1125 /** 1126 * Escape processing state: ESC [ ? 1127 */ 1128 private static final int ESC_LEFT_SQUARE_BRACKET_QUESTION_MARK = 6; 1129 1130 /** 1131 * True if the current escape sequence should continue, false if the current 1132 * escape sequence should be terminated. Used when parsing a single 1133 * character. 1134 */ 1135 private boolean mContinueSequence; 1136 1137 /** 1138 * The current state of the escape sequence state machine. 1139 */ 1140 private int mEscapeState; 1141 1142 /** 1143 * Saved state of the cursor row, Used to implement the save/restore cursor 1144 * position escape sequences. 1145 */ 1146 private int mSavedCursorRow; 1147 1148 /** 1149 * Saved state of the cursor column, Used to implement the save/restore 1150 * cursor position escape sequences. 1151 */ 1152 private int mSavedCursorCol; 1153 1154 // DecSet booleans 1155 1156 /** 1157 * This mask indicates 132-column mode is set. (As opposed to 80-column 1158 * mode.) 1159 */ 1160 private static final int K_132_COLUMN_MODE_MASK = 1 << 3; 1161 1162 /** 1163 * This mask indicates that origin mode is set. (Cursor addressing is 1164 * relative to the absolute screen size, rather than the currently set top 1165 * and bottom margins.) 1166 */ 1167 private static final int K_ORIGIN_MODE_MASK = 1 << 6; 1168 1169 /** 1170 * This mask indicates that wraparound mode is set. (As opposed to 1171 * stop-at-right-column mode.) 1172 */ 1173 private static final int K_WRAPAROUND_MODE_MASK = 1 << 7; 1174 1175 /** 1176 * Holds multiple DECSET flags. The data is stored this way, rather than in 1177 * separate booleans, to make it easier to implement the save-and-restore 1178 * semantics. The various k*ModeMask masks can be used to extract and modify 1179 * the individual flags current states. 1180 */ 1181 private int mDecFlags; 1182 1183 /** 1184 * Saves away a snapshot of the DECSET flags. Used to implement save and 1185 * restore escape sequences. 1186 */ 1187 private int mSavedDecFlags; 1188 1189 // Modes set with Set Mode / Reset Mode 1190 1191 /** 1192 * True if insert mode (as opposed to replace mode) is active. In insert 1193 * mode new characters are inserted, pushing existing text to the right. 1194 */ 1195 private boolean mInsertMode; 1196 1197 /** 1198 * Automatic newline mode. Configures whether pressing return on the 1199 * keyboard automatically generates a return as well. Not currently 1200 * implemented. 1201 */ 1202 private boolean mAutomaticNewlineMode; 1203 1204 /** 1205 * An array of tab stops. mTabStop[i] is true if there is a tab stop set for 1206 * column i. 1207 */ 1208 private boolean[] mTabStop; 1209 1210 // The margins allow portions of the screen to be locked. 1211 1212 /** 1213 * The top margin of the screen, for scrolling purposes. Ranges from 0 to 1214 * mRows-2. 1215 */ 1216 private int mTopMargin; 1217 1218 /** 1219 * The bottom margin of the screen, for scrolling purposes. Ranges from 1220 * mTopMargin + 2 to mRows. (Defines the first row after the scrolling 1221 * region. 1222 */ 1223 private int mBottomMargin; 1224 1225 /** 1226 * True if the next character to be emitted will be automatically wrapped to 1227 * the next line. Used to disambiguate the case where the cursor is 1228 * positioned on column mColumns-1. 1229 */ 1230 private boolean mAboutToAutoWrap; 1231 1232 /** 1233 * Used for debugging, counts how many chars have been processed. 1234 */ 1235 private int mProcessedCharCount; 1236 1237 /** 1238 * Foreground color, 0..7, mask with 8 for bold 1239 */ 1240 private int mForeColor; 1241 1242 /** 1243 * Background color, 0..7, mask with 8 for underline 1244 */ 1245 private int mBackColor; 1246 1247 private boolean mInverseColors; 1248 1249 private boolean mbKeypadApplicationMode; 1250 1251 private boolean mAlternateCharSet; 1252 1253 /** 1254 * Construct a terminal emulator that uses the supplied screen 1255 * 1256 * @param screen the screen to render characters into. 1257 * @param columns the number of columns to emulate 1258 * @param rows the number of rows to emulate 1259 * @param termOut the output file descriptor that talks to the pseudo-tty. 1260 */ TerminalEmulator(Screen screen, int columns, int rows, FileOutputStream termOut)1261 public TerminalEmulator(Screen screen, int columns, int rows, 1262 FileOutputStream termOut) { 1263 mScreen = screen; 1264 mRows = rows; 1265 mColumns = columns; 1266 mTabStop = new boolean[mColumns]; 1267 mTermOut = termOut; 1268 reset(); 1269 } 1270 updateSize(int columns, int rows)1271 public void updateSize(int columns, int rows) { 1272 if (mRows == rows && mColumns == columns) { 1273 return; 1274 } 1275 String transcriptText = mScreen.getTranscriptText(); 1276 1277 mScreen.resize(columns, rows, mForeColor, mBackColor); 1278 1279 if (mRows != rows) { 1280 mRows = rows; 1281 mTopMargin = 0; 1282 mBottomMargin = mRows; 1283 } 1284 if (mColumns != columns) { 1285 int oldColumns = mColumns; 1286 mColumns = columns; 1287 boolean[] oldTabStop = mTabStop; 1288 mTabStop = new boolean[mColumns]; 1289 int toTransfer = Math.min(oldColumns, columns); 1290 System.arraycopy(oldTabStop, 0, mTabStop, 0, toTransfer); 1291 while (mCursorCol >= columns) { 1292 mCursorCol -= columns; 1293 mCursorRow = Math.min(mBottomMargin-1, mCursorRow + 1); 1294 } 1295 } 1296 mCursorRow = 0; 1297 mCursorCol = 0; 1298 mAboutToAutoWrap = false; 1299 1300 int end = transcriptText.length()-1; 1301 while ((end >= 0) && transcriptText.charAt(end) == '\n') { 1302 end--; 1303 } 1304 for(int i = 0; i <= end; i++) { 1305 byte c = (byte) transcriptText.charAt(i); 1306 if (c == '\n') { 1307 setCursorCol(0); 1308 doLinefeed(); 1309 } else { 1310 emit(c); 1311 } 1312 } 1313 } 1314 1315 /** 1316 * Get the cursor's current row. 1317 * 1318 * @return the cursor's current row. 1319 */ getCursorRow()1320 public final int getCursorRow() { 1321 return mCursorRow; 1322 } 1323 1324 /** 1325 * Get the cursor's current column. 1326 * 1327 * @return the cursor's current column. 1328 */ getCursorCol()1329 public final int getCursorCol() { 1330 return mCursorCol; 1331 } 1332 getKeypadApplicationMode()1333 public final boolean getKeypadApplicationMode() { 1334 return mbKeypadApplicationMode; 1335 } 1336 setDefaultTabStops()1337 private void setDefaultTabStops() { 1338 for (int i = 0; i < mColumns; i++) { 1339 mTabStop[i] = (i & 7) == 0 && i != 0; 1340 } 1341 } 1342 1343 /** 1344 * Accept bytes (typically from the pseudo-teletype) and process them. 1345 * 1346 * @param buffer a byte array containing the bytes to be processed 1347 * @param base the first index of the array to process 1348 * @param length the number of bytes in the array to process 1349 */ append(byte[] buffer, int base, int length)1350 public void append(byte[] buffer, int base, int length) { 1351 for (int i = 0; i < length; i++) { 1352 byte b = buffer[base + i]; 1353 try { 1354 if (Term.LOG_CHARACTERS_FLAG) { 1355 char printableB = (char) b; 1356 if (b < 32 || b > 126) { 1357 printableB = ' '; 1358 } 1359 Log.w(Term.LOG_TAG, "'" + Character.toString(printableB) 1360 + "' (" + Integer.toString(b) + ")"); 1361 } 1362 process(b); 1363 mProcessedCharCount++; 1364 } catch (Exception e) { 1365 Log.e(Term.LOG_TAG, "Exception while processing character " 1366 + Integer.toString(mProcessedCharCount) + " code " 1367 + Integer.toString(b), e); 1368 } 1369 } 1370 } 1371 process(byte b)1372 private void process(byte b) { 1373 switch (b) { 1374 case 0: // NUL 1375 // Do nothing 1376 break; 1377 1378 case 7: // BEL 1379 // Do nothing 1380 break; 1381 1382 case 8: // BS 1383 setCursorCol(Math.max(0, mCursorCol - 1)); 1384 break; 1385 1386 case 9: // HT 1387 // Move to next tab stop, but not past edge of screen 1388 setCursorCol(nextTabStop(mCursorCol)); 1389 break; 1390 1391 case 13: 1392 setCursorCol(0); 1393 break; 1394 1395 case 10: // CR 1396 case 11: // VT 1397 case 12: // LF 1398 doLinefeed(); 1399 break; 1400 1401 case 14: // SO: 1402 setAltCharSet(true); 1403 break; 1404 1405 case 15: // SI: 1406 setAltCharSet(false); 1407 break; 1408 1409 1410 case 24: // CAN 1411 case 26: // SUB 1412 if (mEscapeState != ESC_NONE) { 1413 mEscapeState = ESC_NONE; 1414 emit((byte) 127); 1415 } 1416 break; 1417 1418 case 27: // ESC 1419 // Always starts an escape sequence 1420 startEscapeSequence(ESC); 1421 break; 1422 1423 case (byte) 0x9b: // CSI 1424 startEscapeSequence(ESC_LEFT_SQUARE_BRACKET); 1425 break; 1426 1427 default: 1428 mContinueSequence = false; 1429 switch (mEscapeState) { 1430 case ESC_NONE: 1431 if (b >= 32) { 1432 emit(b); 1433 } 1434 break; 1435 1436 case ESC: 1437 doEsc(b); 1438 break; 1439 1440 case ESC_POUND: 1441 doEscPound(b); 1442 break; 1443 1444 case ESC_SELECT_LEFT_PAREN: 1445 doEscSelectLeftParen(b); 1446 break; 1447 1448 case ESC_SELECT_RIGHT_PAREN: 1449 doEscSelectRightParen(b); 1450 break; 1451 1452 case ESC_LEFT_SQUARE_BRACKET: 1453 doEscLeftSquareBracket(b); 1454 break; 1455 1456 case ESC_LEFT_SQUARE_BRACKET_QUESTION_MARK: 1457 doEscLSBQuest(b); 1458 break; 1459 1460 default: 1461 unknownSequence(b); 1462 break; 1463 } 1464 if (!mContinueSequence) { 1465 mEscapeState = ESC_NONE; 1466 } 1467 break; 1468 } 1469 } 1470 setAltCharSet(boolean alternateCharSet)1471 private void setAltCharSet(boolean alternateCharSet) { 1472 mAlternateCharSet = alternateCharSet; 1473 } 1474 nextTabStop(int cursorCol)1475 private int nextTabStop(int cursorCol) { 1476 for (int i = cursorCol; i < mColumns; i++) { 1477 if (mTabStop[i]) { 1478 return i; 1479 } 1480 } 1481 return mColumns - 1; 1482 } 1483 doEscLSBQuest(byte b)1484 private void doEscLSBQuest(byte b) { 1485 int mask = getDecFlagsMask(getArg0(0)); 1486 switch (b) { 1487 case 'h': // Esc [ ? Pn h - DECSET 1488 mDecFlags |= mask; 1489 break; 1490 1491 case 'l': // Esc [ ? Pn l - DECRST 1492 mDecFlags &= ~mask; 1493 break; 1494 1495 case 'r': // Esc [ ? Pn r - restore 1496 mDecFlags = (mDecFlags & ~mask) | (mSavedDecFlags & mask); 1497 break; 1498 1499 case 's': // Esc [ ? Pn s - save 1500 mSavedDecFlags = (mSavedDecFlags & ~mask) | (mDecFlags & mask); 1501 break; 1502 1503 default: 1504 parseArg(b); 1505 break; 1506 } 1507 1508 // 132 column mode 1509 if ((mask & K_132_COLUMN_MODE_MASK) != 0) { 1510 // We don't actually set 132 cols, but we do want the 1511 // side effect of clearing the screen and homing the cursor. 1512 blockClear(0, 0, mColumns, mRows); 1513 setCursorRowCol(0, 0); 1514 } 1515 1516 // origin mode 1517 if ((mask & K_ORIGIN_MODE_MASK) != 0) { 1518 // Home the cursor. 1519 setCursorPosition(0, 0); 1520 } 1521 } 1522 getDecFlagsMask(int argument)1523 private int getDecFlagsMask(int argument) { 1524 if (argument >= 1 && argument <= 9) { 1525 return (1 << argument); 1526 } 1527 1528 return 0; 1529 } 1530 startEscapeSequence(int escapeState)1531 private void startEscapeSequence(int escapeState) { 1532 mEscapeState = escapeState; 1533 mArgIndex = 0; 1534 for (int j = 0; j < MAX_ESCAPE_PARAMETERS; j++) { 1535 mArgs[j] = -1; 1536 } 1537 } 1538 doLinefeed()1539 private void doLinefeed() { 1540 int newCursorRow = mCursorRow + 1; 1541 if (newCursorRow >= mBottomMargin) { 1542 scroll(); 1543 newCursorRow = mBottomMargin - 1; 1544 } 1545 setCursorRow(newCursorRow); 1546 } 1547 continueSequence()1548 private void continueSequence() { 1549 mContinueSequence = true; 1550 } 1551 continueSequence(int state)1552 private void continueSequence(int state) { 1553 mEscapeState = state; 1554 mContinueSequence = true; 1555 } 1556 doEscSelectLeftParen(byte b)1557 private void doEscSelectLeftParen(byte b) { 1558 doSelectCharSet(true, b); 1559 } 1560 doEscSelectRightParen(byte b)1561 private void doEscSelectRightParen(byte b) { 1562 doSelectCharSet(false, b); 1563 } 1564 doSelectCharSet(boolean isG0CharSet, byte b)1565 private void doSelectCharSet(boolean isG0CharSet, byte b) { 1566 switch (b) { 1567 case 'A': // United Kingdom character set 1568 break; 1569 case 'B': // ASCII set 1570 break; 1571 case '0': // Special Graphics 1572 break; 1573 case '1': // Alternate character set 1574 break; 1575 case '2': 1576 break; 1577 default: 1578 unknownSequence(b); 1579 } 1580 } 1581 doEscPound(byte b)1582 private void doEscPound(byte b) { 1583 switch (b) { 1584 case '8': // Esc # 8 - DECALN alignment test 1585 mScreen.blockSet(0, 0, mColumns, mRows, 'E', 1586 getForeColor(), getBackColor()); 1587 break; 1588 1589 default: 1590 unknownSequence(b); 1591 break; 1592 } 1593 } 1594 doEsc(byte b)1595 private void doEsc(byte b) { 1596 switch (b) { 1597 case '#': 1598 continueSequence(ESC_POUND); 1599 break; 1600 1601 case '(': 1602 continueSequence(ESC_SELECT_LEFT_PAREN); 1603 break; 1604 1605 case ')': 1606 continueSequence(ESC_SELECT_RIGHT_PAREN); 1607 break; 1608 1609 case '7': // DECSC save cursor 1610 mSavedCursorRow = mCursorRow; 1611 mSavedCursorCol = mCursorCol; 1612 break; 1613 1614 case '8': // DECRC restore cursor 1615 setCursorRowCol(mSavedCursorRow, mSavedCursorCol); 1616 break; 1617 1618 case 'D': // INDEX 1619 doLinefeed(); 1620 break; 1621 1622 case 'E': // NEL 1623 setCursorCol(0); 1624 doLinefeed(); 1625 break; 1626 1627 case 'F': // Cursor to lower-left corner of screen 1628 setCursorRowCol(0, mBottomMargin - 1); 1629 break; 1630 1631 case 'H': // Tab set 1632 mTabStop[mCursorCol] = true; 1633 break; 1634 1635 case 'M': // Reverse index 1636 if (mCursorRow == 0) { 1637 mScreen.blockCopy(0, mTopMargin + 1, mColumns, mBottomMargin 1638 - (mTopMargin + 1), 0, mTopMargin); 1639 blockClear(0, mBottomMargin - 1, mColumns); 1640 } else { 1641 mCursorRow--; 1642 } 1643 1644 break; 1645 1646 case 'N': // SS2 1647 unimplementedSequence(b); 1648 break; 1649 1650 case '0': // SS3 1651 unimplementedSequence(b); 1652 break; 1653 1654 case 'P': // Device control string 1655 unimplementedSequence(b); 1656 break; 1657 1658 case 'Z': // return terminal ID 1659 sendDeviceAttributes(); 1660 break; 1661 1662 case '[': 1663 continueSequence(ESC_LEFT_SQUARE_BRACKET); 1664 break; 1665 1666 case '=': // DECKPAM 1667 mbKeypadApplicationMode = true; 1668 break; 1669 1670 case '>' : // DECKPNM 1671 mbKeypadApplicationMode = false; 1672 break; 1673 1674 default: 1675 unknownSequence(b); 1676 break; 1677 } 1678 } 1679 doEscLeftSquareBracket(byte b)1680 private void doEscLeftSquareBracket(byte b) { 1681 switch (b) { 1682 case '@': // ESC [ Pn @ - ICH Insert Characters 1683 { 1684 int charsAfterCursor = mColumns - mCursorCol; 1685 int charsToInsert = Math.min(getArg0(1), charsAfterCursor); 1686 int charsToMove = charsAfterCursor - charsToInsert; 1687 mScreen.blockCopy(mCursorCol, mCursorRow, charsToMove, 1, 1688 mCursorCol + charsToInsert, mCursorRow); 1689 blockClear(mCursorCol, mCursorRow, charsToInsert); 1690 } 1691 break; 1692 1693 case 'A': // ESC [ Pn A - Cursor Up 1694 setCursorRow(Math.max(mTopMargin, mCursorRow - getArg0(1))); 1695 break; 1696 1697 case 'B': // ESC [ Pn B - Cursor Down 1698 setCursorRow(Math.min(mBottomMargin - 1, mCursorRow + getArg0(1))); 1699 break; 1700 1701 case 'C': // ESC [ Pn C - Cursor Right 1702 setCursorCol(Math.min(mColumns - 1, mCursorCol + getArg0(1))); 1703 break; 1704 1705 case 'D': // ESC [ Pn D - Cursor Left 1706 setCursorCol(Math.max(0, mCursorCol - getArg0(1))); 1707 break; 1708 1709 case 'G': // ESC [ Pn G - Cursor Horizontal Absolute 1710 setCursorCol(Math.min(Math.max(1, getArg0(1)), mColumns) - 1); 1711 break; 1712 1713 case 'H': // ESC [ Pn ; H - Cursor Position 1714 setHorizontalVerticalPosition(); 1715 break; 1716 1717 case 'J': // ESC [ Pn J - Erase in Display 1718 switch (getArg0(0)) { 1719 case 0: // Clear below 1720 blockClear(mCursorCol, mCursorRow, mColumns - mCursorCol); 1721 blockClear(0, mCursorRow + 1, mColumns, 1722 mBottomMargin - (mCursorRow + 1)); 1723 break; 1724 1725 case 1: // Erase from the start of the screen to the cursor. 1726 blockClear(0, mTopMargin, mColumns, mCursorRow - mTopMargin); 1727 blockClear(0, mCursorRow, mCursorCol + 1); 1728 break; 1729 1730 case 2: // Clear all 1731 blockClear(0, mTopMargin, mColumns, mBottomMargin - mTopMargin); 1732 break; 1733 1734 default: 1735 unknownSequence(b); 1736 break; 1737 } 1738 break; 1739 1740 case 'K': // ESC [ Pn K - Erase in Line 1741 switch (getArg0(0)) { 1742 case 0: // Clear to right 1743 blockClear(mCursorCol, mCursorRow, mColumns - mCursorCol); 1744 break; 1745 1746 case 1: // Erase start of line to cursor (including cursor) 1747 blockClear(0, mCursorRow, mCursorCol + 1); 1748 break; 1749 1750 case 2: // Clear whole line 1751 blockClear(0, mCursorRow, mColumns); 1752 break; 1753 1754 default: 1755 unknownSequence(b); 1756 break; 1757 } 1758 break; 1759 1760 case 'L': // Insert Lines 1761 { 1762 int linesAfterCursor = mBottomMargin - mCursorRow; 1763 int linesToInsert = Math.min(getArg0(1), linesAfterCursor); 1764 int linesToMove = linesAfterCursor - linesToInsert; 1765 mScreen.blockCopy(0, mCursorRow, mColumns, linesToMove, 0, 1766 mCursorRow + linesToInsert); 1767 blockClear(0, mCursorRow, mColumns, linesToInsert); 1768 } 1769 break; 1770 1771 case 'M': // Delete Lines 1772 { 1773 int linesAfterCursor = mBottomMargin - mCursorRow; 1774 int linesToDelete = Math.min(getArg0(1), linesAfterCursor); 1775 int linesToMove = linesAfterCursor - linesToDelete; 1776 mScreen.blockCopy(0, mCursorRow + linesToDelete, mColumns, 1777 linesToMove, 0, mCursorRow); 1778 blockClear(0, mCursorRow + linesToMove, mColumns, linesToDelete); 1779 } 1780 break; 1781 1782 case 'P': // Delete Characters 1783 { 1784 int charsAfterCursor = mColumns - mCursorCol; 1785 int charsToDelete = Math.min(getArg0(1), charsAfterCursor); 1786 int charsToMove = charsAfterCursor - charsToDelete; 1787 mScreen.blockCopy(mCursorCol + charsToDelete, mCursorRow, 1788 charsToMove, 1, mCursorCol, mCursorRow); 1789 blockClear(mCursorCol + charsToMove, mCursorRow, charsToDelete); 1790 } 1791 break; 1792 1793 case 'T': // Mouse tracking 1794 unimplementedSequence(b); 1795 break; 1796 1797 case '?': // Esc [ ? -- start of a private mode set 1798 continueSequence(ESC_LEFT_SQUARE_BRACKET_QUESTION_MARK); 1799 break; 1800 1801 case 'c': // Send device attributes 1802 sendDeviceAttributes(); 1803 break; 1804 1805 case 'd': // ESC [ Pn d - Vert Position Absolute 1806 setCursorRow(Math.min(Math.max(1, getArg0(1)), mRows) - 1); 1807 break; 1808 1809 case 'f': // Horizontal and Vertical Position 1810 setHorizontalVerticalPosition(); 1811 break; 1812 1813 case 'g': // Clear tab stop 1814 switch (getArg0(0)) { 1815 case 0: 1816 mTabStop[mCursorCol] = false; 1817 break; 1818 1819 case 3: 1820 for (int i = 0; i < mColumns; i++) { 1821 mTabStop[i] = false; 1822 } 1823 break; 1824 1825 default: 1826 // Specified to have no effect. 1827 break; 1828 } 1829 break; 1830 1831 case 'h': // Set Mode 1832 doSetMode(true); 1833 break; 1834 1835 case 'l': // Reset Mode 1836 doSetMode(false); 1837 break; 1838 1839 case 'm': // Esc [ Pn m - character attributes. 1840 selectGraphicRendition(); 1841 break; 1842 1843 case 'r': // Esc [ Pn ; Pn r - set top and bottom margins 1844 { 1845 // The top margin defaults to 1, the bottom margin 1846 // (unusually for arguments) defaults to mRows. 1847 // 1848 // The escape sequence numbers top 1..23, but we 1849 // number top 0..22. 1850 // The escape sequence numbers bottom 2..24, and 1851 // so do we (because we use a zero based numbering 1852 // scheme, but we store the first line below the 1853 // bottom-most scrolling line. 1854 // As a result, we adjust the top line by -1, but 1855 // we leave the bottom line alone. 1856 // 1857 // Also require that top + 2 <= bottom 1858 1859 int top = Math.max(0, Math.min(getArg0(1) - 1, mRows - 2)); 1860 int bottom = Math.max(top + 2, Math.min(getArg1(mRows), mRows)); 1861 mTopMargin = top; 1862 mBottomMargin = bottom; 1863 1864 // The cursor is placed in the home position 1865 setCursorRowCol(mTopMargin, 0); 1866 } 1867 break; 1868 1869 default: 1870 parseArg(b); 1871 break; 1872 } 1873 } 1874 selectGraphicRendition()1875 private void selectGraphicRendition() { 1876 for (int i = 0; i <= mArgIndex; i++) { 1877 int code = mArgs[i]; 1878 if ( code < 0) { 1879 if (mArgIndex > 0) { 1880 continue; 1881 } else { 1882 code = 0; 1883 } 1884 } 1885 if (code == 0) { // reset 1886 mInverseColors = false; 1887 mForeColor = 7; 1888 mBackColor = 0; 1889 } else if (code == 1) { // bold 1890 mForeColor |= 0x8; 1891 } else if (code == 4) { // underscore 1892 mBackColor |= 0x8; 1893 } else if (code == 7) { // inverse 1894 mInverseColors = true; 1895 } else if (code >= 30 && code <= 37) { // foreground color 1896 mForeColor = (mForeColor & 0x8) | (code - 30); 1897 } else if (code >= 40 && code <= 47) { // background color 1898 mBackColor = (mBackColor & 0x8) | (code - 40); 1899 } else { 1900 if (Term.LOG_UNKNOWN_ESCAPE_SEQUENCES) { 1901 Log.w(Term.LOG_TAG, String.format("SGR unknown code %d", code)); 1902 } 1903 } 1904 } 1905 } 1906 blockClear(int sx, int sy, int w)1907 private void blockClear(int sx, int sy, int w) { 1908 blockClear(sx, sy, w, 1); 1909 } 1910 blockClear(int sx, int sy, int w, int h)1911 private void blockClear(int sx, int sy, int w, int h) { 1912 mScreen.blockSet(sx, sy, w, h, ' ', getForeColor(), getBackColor()); 1913 } 1914 getForeColor()1915 private int getForeColor() { 1916 return mInverseColors ? 1917 ((mBackColor & 0x7) | (mForeColor & 0x8)) : mForeColor; 1918 } 1919 getBackColor()1920 private int getBackColor() { 1921 return mInverseColors ? 1922 ((mForeColor & 0x7) | (mBackColor & 0x8)) : mBackColor; 1923 } 1924 doSetMode(boolean newValue)1925 private void doSetMode(boolean newValue) { 1926 int modeBit = getArg0(0); 1927 switch (modeBit) { 1928 case 4: 1929 mInsertMode = newValue; 1930 break; 1931 1932 case 20: 1933 mAutomaticNewlineMode = newValue; 1934 break; 1935 1936 default: 1937 unknownParameter(modeBit); 1938 break; 1939 } 1940 } 1941 setHorizontalVerticalPosition()1942 private void setHorizontalVerticalPosition() { 1943 1944 // Parameters are Row ; Column 1945 1946 setCursorPosition(getArg1(1) - 1, getArg0(1) - 1); 1947 } 1948 setCursorPosition(int x, int y)1949 private void setCursorPosition(int x, int y) { 1950 int effectiveTopMargin = 0; 1951 int effectiveBottomMargin = mRows; 1952 if ((mDecFlags & K_ORIGIN_MODE_MASK) != 0) { 1953 effectiveTopMargin = mTopMargin; 1954 effectiveBottomMargin = mBottomMargin; 1955 } 1956 int newRow = 1957 Math.max(effectiveTopMargin, Math.min(effectiveTopMargin + y, 1958 effectiveBottomMargin - 1)); 1959 int newCol = Math.max(0, Math.min(x, mColumns - 1)); 1960 setCursorRowCol(newRow, newCol); 1961 } 1962 sendDeviceAttributes()1963 private void sendDeviceAttributes() { 1964 // This identifies us as a DEC vt100 with advanced 1965 // video options. This is what the xterm terminal 1966 // emulator sends. 1967 byte[] attributes = 1968 { 1969 /* VT100 */ 1970 (byte) 27, (byte) '[', (byte) '?', (byte) '1', 1971 (byte) ';', (byte) '2', (byte) 'c' 1972 1973 /* VT220 1974 (byte) 27, (byte) '[', (byte) '?', (byte) '6', 1975 (byte) '0', (byte) ';', 1976 (byte) '1', (byte) ';', 1977 (byte) '2', (byte) ';', 1978 (byte) '6', (byte) ';', 1979 (byte) '8', (byte) ';', 1980 (byte) '9', (byte) ';', 1981 (byte) '1', (byte) '5', (byte) ';', 1982 (byte) 'c' 1983 */ 1984 }; 1985 1986 write(attributes); 1987 } 1988 1989 /** 1990 * Send data to the shell process 1991 * @param data 1992 */ write(byte[] data)1993 private void write(byte[] data) { 1994 try { 1995 mTermOut.write(data); 1996 mTermOut.flush(); 1997 } catch (IOException e) { 1998 // Ignore exception 1999 // We don't really care if the receiver isn't listening. 2000 // We just make a best effort to answer the query. 2001 } 2002 } 2003 scroll()2004 private void scroll() { 2005 mScreen.scroll(mTopMargin, mBottomMargin, 2006 getForeColor(), getBackColor()); 2007 } 2008 2009 /** 2010 * Process the next ASCII character of a parameter. 2011 * 2012 * @param b The next ASCII character of the paramater sequence. 2013 */ parseArg(byte b)2014 private void parseArg(byte b) { 2015 if (b >= '0' && b <= '9') { 2016 if (mArgIndex < mArgs.length) { 2017 int oldValue = mArgs[mArgIndex]; 2018 int thisDigit = b - '0'; 2019 int value; 2020 if (oldValue >= 0) { 2021 value = oldValue * 10 + thisDigit; 2022 } else { 2023 value = thisDigit; 2024 } 2025 mArgs[mArgIndex] = value; 2026 } 2027 continueSequence(); 2028 } else if (b == ';') { 2029 if (mArgIndex < mArgs.length) { 2030 mArgIndex++; 2031 } 2032 continueSequence(); 2033 } else { 2034 unknownSequence(b); 2035 } 2036 } 2037 getArg0(int defaultValue)2038 private int getArg0(int defaultValue) { 2039 return getArg(0, defaultValue); 2040 } 2041 getArg1(int defaultValue)2042 private int getArg1(int defaultValue) { 2043 return getArg(1, defaultValue); 2044 } 2045 getArg(int index, int defaultValue)2046 private int getArg(int index, int defaultValue) { 2047 int result = mArgs[index]; 2048 if (result < 0) { 2049 result = defaultValue; 2050 } 2051 return result; 2052 } 2053 unimplementedSequence(byte b)2054 private void unimplementedSequence(byte b) { 2055 if (Term.LOG_UNKNOWN_ESCAPE_SEQUENCES) { 2056 logError("unimplemented", b); 2057 } 2058 finishSequence(); 2059 } 2060 unknownSequence(byte b)2061 private void unknownSequence(byte b) { 2062 if (Term.LOG_UNKNOWN_ESCAPE_SEQUENCES) { 2063 logError("unknown", b); 2064 } 2065 finishSequence(); 2066 } 2067 unknownParameter(int parameter)2068 private void unknownParameter(int parameter) { 2069 if (Term.LOG_UNKNOWN_ESCAPE_SEQUENCES) { 2070 StringBuilder buf = new StringBuilder(); 2071 buf.append("Unknown parameter"); 2072 buf.append(parameter); 2073 logError(buf.toString()); 2074 } 2075 } 2076 logError(String errorType, byte b)2077 private void logError(String errorType, byte b) { 2078 if (Term.LOG_UNKNOWN_ESCAPE_SEQUENCES) { 2079 StringBuilder buf = new StringBuilder(); 2080 buf.append(errorType); 2081 buf.append(" sequence "); 2082 buf.append(" EscapeState: "); 2083 buf.append(mEscapeState); 2084 buf.append(" char: '"); 2085 buf.append((char) b); 2086 buf.append("' ("); 2087 buf.append(b); 2088 buf.append(")"); 2089 boolean firstArg = true; 2090 for (int i = 0; i <= mArgIndex; i++) { 2091 int value = mArgs[i]; 2092 if (value >= 0) { 2093 if (firstArg) { 2094 firstArg = false; 2095 buf.append("args = "); 2096 } 2097 buf.append(String.format("%d; ", value)); 2098 } 2099 } 2100 logError(buf.toString()); 2101 } 2102 } 2103 logError(String error)2104 private void logError(String error) { 2105 if (Term.LOG_UNKNOWN_ESCAPE_SEQUENCES) { 2106 Log.e(Term.LOG_TAG, error); 2107 } 2108 finishSequence(); 2109 } 2110 finishSequence()2111 private void finishSequence() { 2112 mEscapeState = ESC_NONE; 2113 } 2114 autoWrapEnabled()2115 private boolean autoWrapEnabled() { 2116 // Always enable auto wrap, because it's useful on a small screen 2117 return true; 2118 // return (mDecFlags & K_WRAPAROUND_MODE_MASK) != 0; 2119 } 2120 2121 /** 2122 * Send an ASCII character to the screen. 2123 * 2124 * @param b the ASCII character to display. 2125 */ emit(byte b)2126 private void emit(byte b) { 2127 boolean autoWrap = autoWrapEnabled(); 2128 2129 if (autoWrap) { 2130 if (mCursorCol == mColumns - 1 && mAboutToAutoWrap) { 2131 mScreen.setLineWrap(mCursorRow); 2132 mCursorCol = 0; 2133 if (mCursorRow + 1 < mBottomMargin) { 2134 mCursorRow++; 2135 } else { 2136 scroll(); 2137 } 2138 } 2139 } 2140 2141 if (mInsertMode) { // Move character to right one space 2142 int destCol = mCursorCol + 1; 2143 if (destCol < mColumns) { 2144 mScreen.blockCopy(mCursorCol, mCursorRow, mColumns - destCol, 2145 1, destCol, mCursorRow); 2146 } 2147 } 2148 2149 mScreen.set(mCursorCol, mCursorRow, b, getForeColor(), getBackColor()); 2150 2151 if (autoWrap) { 2152 mAboutToAutoWrap = (mCursorCol == mColumns - 1); 2153 } 2154 2155 mCursorCol = Math.min(mCursorCol + 1, mColumns - 1); 2156 } 2157 setCursorRow(int row)2158 private void setCursorRow(int row) { 2159 mCursorRow = row; 2160 mAboutToAutoWrap = false; 2161 } 2162 setCursorCol(int col)2163 private void setCursorCol(int col) { 2164 mCursorCol = col; 2165 mAboutToAutoWrap = false; 2166 } 2167 setCursorRowCol(int row, int col)2168 private void setCursorRowCol(int row, int col) { 2169 mCursorRow = Math.min(row, mRows-1); 2170 mCursorCol = Math.min(col, mColumns-1); 2171 mAboutToAutoWrap = false; 2172 } 2173 2174 /** 2175 * Reset the terminal emulator to its initial state. 2176 */ reset()2177 public void reset() { 2178 mCursorRow = 0; 2179 mCursorCol = 0; 2180 mArgIndex = 0; 2181 mContinueSequence = false; 2182 mEscapeState = ESC_NONE; 2183 mSavedCursorRow = 0; 2184 mSavedCursorCol = 0; 2185 mDecFlags = 0; 2186 mSavedDecFlags = 0; 2187 mInsertMode = false; 2188 mAutomaticNewlineMode = false; 2189 mTopMargin = 0; 2190 mBottomMargin = mRows; 2191 mAboutToAutoWrap = false; 2192 mForeColor = 7; 2193 mBackColor = 0; 2194 mInverseColors = false; 2195 mbKeypadApplicationMode = false; 2196 mAlternateCharSet = false; 2197 // mProcessedCharCount is preserved unchanged. 2198 setDefaultTabStops(); 2199 blockClear(0, 0, mColumns, mRows); 2200 } 2201 getTranscriptText()2202 public String getTranscriptText() { 2203 return mScreen.getTranscriptText(); 2204 } 2205 } 2206 2207 /** 2208 * Text renderer interface 2209 */ 2210 2211 interface TextRenderer { getCharacterWidth()2212 int getCharacterWidth(); getCharacterHeight()2213 int getCharacterHeight(); drawTextRun(Canvas canvas, float x, float y, int lineOffset, char[] text, int index, int count, boolean cursor, int foreColor, int backColor)2214 void drawTextRun(Canvas canvas, float x, float y, 2215 int lineOffset, char[] text, 2216 int index, int count, boolean cursor, int foreColor, int backColor); 2217 } 2218 2219 abstract class BaseTextRenderer implements TextRenderer { 2220 protected int[] mForePaint = { 2221 0xff000000, // Black 2222 0xffff0000, // Red 2223 0xff00ff00, // green 2224 0xffffff00, // yellow 2225 0xff0000ff, // blue 2226 0xffff00ff, // magenta 2227 0xff00ffff, // cyan 2228 0xffffffff // white -- is overridden by constructor 2229 }; 2230 protected int[] mBackPaint = { 2231 0xff000000, // Black -- is overridden by constructor 2232 0xffcc0000, // Red 2233 0xff00cc00, // green 2234 0xffcccc00, // yellow 2235 0xff0000cc, // blue 2236 0xffff00cc, // magenta 2237 0xff00cccc, // cyan 2238 0xffffffff // white 2239 }; 2240 protected final static int mCursorPaint = 0xff808080; 2241 BaseTextRenderer(int forePaintColor, int backPaintColor)2242 public BaseTextRenderer(int forePaintColor, int backPaintColor) { 2243 mForePaint[7] = forePaintColor; 2244 mBackPaint[0] = backPaintColor; 2245 2246 } 2247 } 2248 2249 class Bitmap4x8FontRenderer extends BaseTextRenderer { 2250 private final static int kCharacterWidth = 4; 2251 private final static int kCharacterHeight = 8; 2252 private Bitmap mFont; 2253 private int mCurrentForeColor; 2254 private int mCurrentBackColor; 2255 private float[] mColorMatrix; 2256 private Paint mPaint; 2257 private static final float BYTE_SCALE = 1.0f / 255.0f; 2258 Bitmap4x8FontRenderer(Resources resources, int forePaintColor, int backPaintColor)2259 public Bitmap4x8FontRenderer(Resources resources, 2260 int forePaintColor, int backPaintColor) { 2261 super(forePaintColor, backPaintColor); 2262 mFont = BitmapFactory.decodeResource(resources, 2263 R.drawable.atari_small); 2264 mPaint = new Paint(); 2265 mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); 2266 } 2267 getCharacterWidth()2268 public int getCharacterWidth() { 2269 return kCharacterWidth; 2270 } 2271 getCharacterHeight()2272 public int getCharacterHeight() { 2273 return kCharacterHeight; 2274 } 2275 drawTextRun(Canvas canvas, float x, float y, int lineOffset, char[] text, int index, int count, boolean cursor, int foreColor, int backColor)2276 public void drawTextRun(Canvas canvas, float x, float y, 2277 int lineOffset, char[] text, int index, int count, 2278 boolean cursor, int foreColor, int backColor) { 2279 setColorMatrix(mForePaint[foreColor & 7], 2280 cursor ? mCursorPaint : mBackPaint[backColor & 7]); 2281 int destX = (int) x + kCharacterWidth * lineOffset; 2282 int destY = (int) y; 2283 Rect srcRect = new Rect(); 2284 Rect destRect = new Rect(); 2285 destRect.top = (destY - kCharacterHeight); 2286 destRect.bottom = destY; 2287 for(int i = 0; i < count; i++) { 2288 char c = text[i + index]; 2289 if ((cursor || (c != 32)) && (c < 128)) { 2290 int cellX = c & 31; 2291 int cellY = (c >> 5) & 3; 2292 int srcX = cellX * kCharacterWidth; 2293 int srcY = cellY * kCharacterHeight; 2294 srcRect.set(srcX, srcY, 2295 srcX + kCharacterWidth, srcY + kCharacterHeight); 2296 destRect.left = destX; 2297 destRect.right = destX + kCharacterWidth; 2298 canvas.drawBitmap(mFont, srcRect, destRect, mPaint); 2299 } 2300 destX += kCharacterWidth; 2301 } 2302 } 2303 setColorMatrix(int foreColor, int backColor)2304 private void setColorMatrix(int foreColor, int backColor) { 2305 if ((foreColor != mCurrentForeColor) 2306 || (backColor != mCurrentBackColor) 2307 || (mColorMatrix == null)) { 2308 mCurrentForeColor = foreColor; 2309 mCurrentBackColor = backColor; 2310 if (mColorMatrix == null) { 2311 mColorMatrix = new float[20]; 2312 mColorMatrix[18] = 1.0f; // Just copy Alpha 2313 } 2314 for (int component = 0; component < 3; component++) { 2315 int rightShift = (2 - component) << 3; 2316 int fore = 0xff & (foreColor >> rightShift); 2317 int back = 0xff & (backColor >> rightShift); 2318 int delta = back - fore; 2319 mColorMatrix[component * 6] = delta * BYTE_SCALE; 2320 mColorMatrix[component * 5 + 4] = fore; 2321 } 2322 mPaint.setColorFilter(new ColorMatrixColorFilter(mColorMatrix)); 2323 } 2324 } 2325 } 2326 2327 class PaintRenderer extends BaseTextRenderer { PaintRenderer(int fontSize, int forePaintColor, int backPaintColor)2328 public PaintRenderer(int fontSize, int forePaintColor, int backPaintColor) { 2329 super(forePaintColor, backPaintColor); 2330 mTextPaint = new Paint(); 2331 mTextPaint.setTypeface(Typeface.MONOSPACE); 2332 mTextPaint.setAntiAlias(true); 2333 mTextPaint.setTextSize(fontSize); 2334 2335 mCharHeight = (int) Math.ceil(mTextPaint.getFontSpacing()); 2336 mCharAscent = (int) Math.ceil(mTextPaint.ascent()); 2337 mCharDescent = mCharHeight + mCharAscent; 2338 mCharWidth = (int) mTextPaint.measureText(EXAMPLE_CHAR, 0, 1); 2339 } 2340 drawTextRun(Canvas canvas, float x, float y, int lineOffset, char[] text, int index, int count, boolean cursor, int foreColor, int backColor)2341 public void drawTextRun(Canvas canvas, float x, float y, int lineOffset, 2342 char[] text, int index, int count, 2343 boolean cursor, int foreColor, int backColor) { 2344 if (cursor) { 2345 mTextPaint.setColor(mCursorPaint); 2346 } else { 2347 mTextPaint.setColor(mBackPaint[backColor & 0x7]); 2348 } 2349 float left = x + lineOffset * mCharWidth; 2350 canvas.drawRect(left, y + mCharAscent, 2351 left + count * mCharWidth, y + mCharDescent, 2352 mTextPaint); 2353 boolean bold = ( foreColor & 0x8 ) != 0; 2354 boolean underline = (backColor & 0x8) != 0; 2355 if (bold) { 2356 mTextPaint.setFakeBoldText(true); 2357 } 2358 if (underline) { 2359 mTextPaint.setUnderlineText(true); 2360 } 2361 mTextPaint.setColor(mForePaint[foreColor & 0x7]); 2362 canvas.drawText(text, index, count, left, y, mTextPaint); 2363 if (bold) { 2364 mTextPaint.setFakeBoldText(false); 2365 } 2366 if (underline) { 2367 mTextPaint.setUnderlineText(false); 2368 } 2369 } 2370 getCharacterHeight()2371 public int getCharacterHeight() { 2372 return mCharHeight; 2373 } 2374 getCharacterWidth()2375 public int getCharacterWidth() { 2376 return mCharWidth; 2377 } 2378 2379 2380 private Paint mTextPaint; 2381 private int mCharWidth; 2382 private int mCharHeight; 2383 private int mCharAscent; 2384 private int mCharDescent; 2385 private static final char[] EXAMPLE_CHAR = {'X'}; 2386 } 2387 2388 /** 2389 * A multi-thread-safe produce-consumer byte array. 2390 * Only allows one producer and one consumer. 2391 */ 2392 2393 class ByteQueue { ByteQueue(int size)2394 public ByteQueue(int size) { 2395 mBuffer = new byte[size]; 2396 } 2397 getBytesAvailable()2398 public int getBytesAvailable() { 2399 synchronized(this) { 2400 return mStoredBytes; 2401 } 2402 } 2403 read(byte[] buffer, int offset, int length)2404 public int read(byte[] buffer, int offset, int length) 2405 throws InterruptedException { 2406 if (length + offset > buffer.length) { 2407 throw 2408 new IllegalArgumentException("length + offset > buffer.length"); 2409 } 2410 if (length < 0) { 2411 throw 2412 new IllegalArgumentException("length < 0"); 2413 2414 } 2415 if (length == 0) { 2416 return 0; 2417 } 2418 synchronized(this) { 2419 while (mStoredBytes == 0) { 2420 wait(); 2421 } 2422 int totalRead = 0; 2423 int bufferLength = mBuffer.length; 2424 boolean wasFull = bufferLength == mStoredBytes; 2425 while (length > 0 && mStoredBytes > 0) { 2426 int oneRun = Math.min(bufferLength - mHead, mStoredBytes); 2427 int bytesToCopy = Math.min(length, oneRun); 2428 System.arraycopy(mBuffer, mHead, buffer, offset, bytesToCopy); 2429 mHead += bytesToCopy; 2430 if (mHead >= bufferLength) { 2431 mHead = 0; 2432 } 2433 mStoredBytes -= bytesToCopy; 2434 length -= bytesToCopy; 2435 offset += bytesToCopy; 2436 totalRead += bytesToCopy; 2437 } 2438 if (wasFull) { 2439 notify(); 2440 } 2441 return totalRead; 2442 } 2443 } 2444 write(byte[] buffer, int offset, int length)2445 public void write(byte[] buffer, int offset, int length) 2446 throws InterruptedException { 2447 if (length + offset > buffer.length) { 2448 throw 2449 new IllegalArgumentException("length + offset > buffer.length"); 2450 } 2451 if (length < 0) { 2452 throw 2453 new IllegalArgumentException("length < 0"); 2454 2455 } 2456 if (length == 0) { 2457 return; 2458 } 2459 synchronized(this) { 2460 int bufferLength = mBuffer.length; 2461 boolean wasEmpty = mStoredBytes == 0; 2462 while (length > 0) { 2463 while(bufferLength == mStoredBytes) { 2464 wait(); 2465 } 2466 int tail = mHead + mStoredBytes; 2467 int oneRun; 2468 if (tail >= bufferLength) { 2469 tail = tail - bufferLength; 2470 oneRun = mHead - tail; 2471 } else { 2472 oneRun = bufferLength - tail; 2473 } 2474 int bytesToCopy = Math.min(oneRun, length); 2475 System.arraycopy(buffer, offset, mBuffer, tail, bytesToCopy); 2476 offset += bytesToCopy; 2477 mStoredBytes += bytesToCopy; 2478 length -= bytesToCopy; 2479 } 2480 if (wasEmpty) { 2481 notify(); 2482 } 2483 } 2484 } 2485 2486 private byte[] mBuffer; 2487 private int mHead; 2488 private int mStoredBytes; 2489 } 2490 /** 2491 * A view on a transcript and a terminal emulator. Displays the text of the 2492 * transcript and the current cursor position of the terminal emulator. 2493 */ 2494 class EmulatorView extends View implements GestureDetector.OnGestureListener { 2495 2496 /** 2497 * We defer some initialization until we have been layed out in the view 2498 * hierarchy. The boolean tracks when we know what our size is. 2499 */ 2500 private boolean mKnownSize; 2501 2502 /** 2503 * Our transcript. Contains the screen and the transcript. 2504 */ 2505 private TranscriptScreen mTranscriptScreen; 2506 2507 /** 2508 * Number of rows in the transcript. 2509 */ 2510 private static final int TRANSCRIPT_ROWS = 10000; 2511 2512 /** 2513 * Total width of each character, in pixels 2514 */ 2515 private int mCharacterWidth; 2516 2517 /** 2518 * Total height of each character, in pixels 2519 */ 2520 private int mCharacterHeight; 2521 2522 /** 2523 * Used to render text 2524 */ 2525 private TextRenderer mTextRenderer; 2526 2527 /** 2528 * Text size. Zero means 4 x 8 font. 2529 */ 2530 private int mTextSize; 2531 2532 /** 2533 * Foreground color. 2534 */ 2535 private int mForeground; 2536 2537 /** 2538 * Background color. 2539 */ 2540 private int mBackground; 2541 2542 /** 2543 * Used to paint the cursor 2544 */ 2545 private Paint mCursorPaint; 2546 2547 private Paint mBackgroundPaint; 2548 2549 /** 2550 * Our terminal emulator. We use this to get the current cursor position. 2551 */ 2552 private TerminalEmulator mEmulator; 2553 2554 /** 2555 * The number of rows of text to display. 2556 */ 2557 private int mRows; 2558 2559 /** 2560 * The number of columns of text to display. 2561 */ 2562 private int mColumns; 2563 2564 /** 2565 * The number of columns that are visible on the display. 2566 */ 2567 2568 private int mVisibleColumns; 2569 2570 /** 2571 * The top row of text to display. Ranges from -activeTranscriptRows to 0 2572 */ 2573 private int mTopRow; 2574 2575 private int mLeftColumn; 2576 2577 private FileDescriptor mTermFd; 2578 /** 2579 * Used to receive data from the remote process. 2580 */ 2581 private FileInputStream mTermIn; 2582 2583 private FileOutputStream mTermOut; 2584 2585 private ByteQueue mByteQueue; 2586 2587 /** 2588 * Used to temporarily hold data received from the remote process. Allocated 2589 * once and used permanently to minimize heap thrashing. 2590 */ 2591 private byte[] mReceiveBuffer; 2592 2593 /** 2594 * Our private message id, which we use to receive new input from the 2595 * remote process. 2596 */ 2597 private static final int UPDATE = 1; 2598 2599 /** 2600 * Thread that polls for input from the remote process 2601 */ 2602 2603 private Thread mPollingThread; 2604 2605 private GestureDetector mGestureDetector; 2606 private float mScrollRemainder; 2607 private TermKeyListener mKeyListener; 2608 2609 /** 2610 * Our message handler class. Implements a periodic callback. 2611 */ 2612 private final Handler mHandler = new Handler() { 2613 /** 2614 * Handle the callback message. Call our enclosing class's update 2615 * method. 2616 * 2617 * @param msg The callback message. 2618 */ 2619 @Override 2620 public void handleMessage(Message msg) { 2621 if (msg.what == UPDATE) { 2622 update(); 2623 } 2624 } 2625 }; 2626 EmulatorView(Context context)2627 public EmulatorView(Context context) { 2628 super(context); 2629 commonConstructor(); 2630 } 2631 register(TermKeyListener listener)2632 public void register(TermKeyListener listener) { 2633 mKeyListener = listener; 2634 } 2635 setColors(int foreground, int background)2636 public void setColors(int foreground, int background) { 2637 mForeground = foreground; 2638 mBackground = background; 2639 updateText(); 2640 } 2641 getTranscriptText()2642 public String getTranscriptText() { 2643 return mEmulator.getTranscriptText(); 2644 } 2645 resetTerminal()2646 public void resetTerminal() { 2647 mEmulator.reset(); 2648 invalidate(); 2649 } 2650 2651 @Override onCheckIsTextEditor()2652 public boolean onCheckIsTextEditor() { 2653 return true; 2654 } 2655 2656 @Override onCreateInputConnection(EditorInfo outAttrs)2657 public InputConnection onCreateInputConnection(EditorInfo outAttrs) { 2658 return new BaseInputConnection(this, false) { 2659 2660 @Override 2661 public boolean beginBatchEdit() { 2662 return true; 2663 } 2664 2665 @Override 2666 public boolean clearMetaKeyStates(int states) { 2667 return true; 2668 } 2669 2670 @Override 2671 public boolean commitCompletion(CompletionInfo text) { 2672 return true; 2673 } 2674 2675 @Override 2676 public boolean commitText(CharSequence text, int newCursorPosition) { 2677 sendText(text); 2678 return true; 2679 } 2680 2681 @Override 2682 public boolean deleteSurroundingText(int leftLength, int rightLength) { 2683 return true; 2684 } 2685 2686 @Override 2687 public boolean endBatchEdit() { 2688 return true; 2689 } 2690 2691 @Override 2692 public boolean finishComposingText() { 2693 return true; 2694 } 2695 2696 @Override 2697 public int getCursorCapsMode(int reqModes) { 2698 return 0; 2699 } 2700 2701 @Override 2702 public ExtractedText getExtractedText(ExtractedTextRequest request, 2703 int flags) { 2704 return null; 2705 } 2706 2707 @Override 2708 public CharSequence getTextAfterCursor(int n, int flags) { 2709 return null; 2710 } 2711 2712 @Override 2713 public CharSequence getTextBeforeCursor(int n, int flags) { 2714 return null; 2715 } 2716 2717 @Override 2718 public boolean performEditorAction(int actionCode) { 2719 if(actionCode == EditorInfo.IME_ACTION_UNSPECIFIED) { 2720 // The "return" key has been pressed on the IME. 2721 sendText("\n"); 2722 return true; 2723 } 2724 return false; 2725 } 2726 2727 @Override 2728 public boolean performContextMenuAction(int id) { 2729 return true; 2730 } 2731 2732 @Override 2733 public boolean performPrivateCommand(String action, Bundle data) { 2734 return true; 2735 } 2736 2737 @Override 2738 public boolean sendKeyEvent(KeyEvent event) { 2739 if (event.getAction() == KeyEvent.ACTION_DOWN) { 2740 switch(event.getKeyCode()) { 2741 case KeyEvent.KEYCODE_DEL: 2742 sendChar(127); 2743 break; 2744 } 2745 } 2746 return true; 2747 } 2748 2749 @Override 2750 public boolean setComposingText(CharSequence text, int newCursorPosition) { 2751 return true; 2752 } 2753 2754 @Override 2755 public boolean setSelection(int start, int end) { 2756 return true; 2757 } 2758 2759 private void sendChar(int c) { 2760 try { 2761 mapAndSend(c); 2762 } catch (IOException ex) { 2763 2764 } 2765 } 2766 private void sendText(CharSequence text) { 2767 int n = text.length(); 2768 try { 2769 for(int i = 0; i < n; i++) { 2770 char c = text.charAt(i); 2771 mapAndSend(c); 2772 } 2773 } catch (IOException e) { 2774 } 2775 } 2776 2777 private void mapAndSend(int c) throws IOException { 2778 mTermOut.write( 2779 mKeyListener.mapControlChar(c)); 2780 } 2781 }; 2782 } 2783 2784 public boolean getKeypadApplicationMode() { 2785 return mEmulator.getKeypadApplicationMode(); 2786 } 2787 2788 public EmulatorView(Context context, AttributeSet attrs) { 2789 this(context, attrs, 0); 2790 } 2791 2792 public EmulatorView(Context context, AttributeSet attrs, 2793 int defStyle) { 2794 super(context, attrs, defStyle); 2795 TypedArray a = 2796 context.obtainStyledAttributes(android.R.styleable.View); 2797 initializeScrollbars(a); 2798 a.recycle(); 2799 commonConstructor(); 2800 } 2801 2802 private void commonConstructor() { 2803 mTextRenderer = null; 2804 mCursorPaint = new Paint(); 2805 mCursorPaint.setARGB(255,128,128,128); 2806 mBackgroundPaint = new Paint(); 2807 mTopRow = 0; 2808 mLeftColumn = 0; 2809 mGestureDetector = new GestureDetector(this); 2810 mGestureDetector.setIsLongpressEnabled(false); 2811 setVerticalScrollBarEnabled(true); 2812 } 2813 2814 @Override 2815 protected int computeVerticalScrollRange() { 2816 return mTranscriptScreen.getActiveRows(); 2817 } 2818 2819 @Override 2820 protected int computeVerticalScrollExtent() { 2821 return mRows; 2822 } 2823 2824 @Override 2825 protected int computeVerticalScrollOffset() { 2826 return mTranscriptScreen.getActiveRows() + mTopRow - mRows; 2827 } 2828 2829 /** 2830 * Call this to initialize the view. 2831 * 2832 * @param termFd the file descriptor 2833 * @param termOut the output stream for the pseudo-teletype 2834 */ 2835 public void initialize(FileDescriptor termFd, FileOutputStream termOut) { 2836 mTermOut = termOut; 2837 mTermFd = termFd; 2838 mTextSize = 10; 2839 mForeground = Term.WHITE; 2840 mBackground = Term.BLACK; 2841 updateText(); 2842 mTermIn = new FileInputStream(mTermFd); 2843 mReceiveBuffer = new byte[4 * 1024]; 2844 mByteQueue = new ByteQueue(4 * 1024); 2845 } 2846 2847 /** 2848 * Accept a sequence of bytes (typically from the pseudo-tty) and process 2849 * them. 2850 * 2851 * @param buffer a byte array containing bytes to be processed 2852 * @param base the index of the first byte in the buffer to process 2853 * @param length the number of bytes to process 2854 */ 2855 public void append(byte[] buffer, int base, int length) { 2856 mEmulator.append(buffer, base, length); 2857 ensureCursorVisible(); 2858 invalidate(); 2859 } 2860 2861 /** 2862 * Page the terminal view (scroll it up or down by delta screenfulls.) 2863 * 2864 * @param delta the number of screens to scroll. Positive means scroll down, 2865 * negative means scroll up. 2866 */ 2867 public void page(int delta) { 2868 mTopRow = 2869 Math.min(0, Math.max(-(mTranscriptScreen 2870 .getActiveTranscriptRows()), mTopRow + mRows * delta)); 2871 invalidate(); 2872 } 2873 2874 /** 2875 * Page the terminal view horizontally. 2876 * 2877 * @param deltaColumns the number of columns to scroll. Positive scrolls to 2878 * the right. 2879 */ 2880 public void pageHorizontal(int deltaColumns) { 2881 mLeftColumn = 2882 Math.max(0, Math.min(mLeftColumn + deltaColumns, mColumns 2883 - mVisibleColumns)); 2884 invalidate(); 2885 } 2886 2887 /** 2888 * Sets the text size, which in turn sets the number of rows and columns 2889 * 2890 * @param fontSize the new font size, in pixels. 2891 */ 2892 public void setTextSize(int fontSize) { 2893 mTextSize = fontSize; 2894 updateText(); 2895 } 2896 2897 // Begin GestureDetector.OnGestureListener methods 2898 2899 public boolean onSingleTapUp(MotionEvent e) { 2900 return true; 2901 } 2902 2903 public void onLongPress(MotionEvent e) { 2904 } 2905 2906 public boolean onScroll(MotionEvent e1, MotionEvent e2, 2907 float distanceX, float distanceY) { 2908 distanceY += mScrollRemainder; 2909 int deltaRows = (int) (distanceY / mCharacterHeight); 2910 mScrollRemainder = distanceY - deltaRows * mCharacterHeight; 2911 mTopRow = 2912 Math.min(0, Math.max(-(mTranscriptScreen 2913 .getActiveTranscriptRows()), mTopRow + deltaRows)); 2914 invalidate(); 2915 2916 return true; 2917 } 2918 2919 public void onSingleTapConfirmed(MotionEvent e) { 2920 } 2921 2922 public boolean onJumpTapDown(MotionEvent e1, MotionEvent e2) { 2923 // Scroll to bottom 2924 mTopRow = 0; 2925 invalidate(); 2926 return true; 2927 } 2928 2929 public boolean onJumpTapUp(MotionEvent e1, MotionEvent e2) { 2930 // Scroll to top 2931 mTopRow = -mTranscriptScreen.getActiveTranscriptRows(); 2932 invalidate(); 2933 return true; 2934 } 2935 2936 public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, 2937 float velocityY) { 2938 // TODO: add animation man's (non animated) fling 2939 mScrollRemainder = 0.0f; 2940 onScroll(e1, e2, 2 * velocityX, -2 * velocityY); 2941 return true; 2942 } 2943 2944 public void onShowPress(MotionEvent e) { 2945 } 2946 2947 public boolean onDown(MotionEvent e) { 2948 mScrollRemainder = 0.0f; 2949 return true; 2950 } 2951 2952 // End GestureDetector.OnGestureListener methods 2953 2954 @Override public boolean onTouchEvent(MotionEvent ev) { 2955 return mGestureDetector.onTouchEvent(ev); 2956 } 2957 2958 private void updateText() { 2959 if (mTextSize > 0) { 2960 mTextRenderer = new PaintRenderer(mTextSize, mForeground, 2961 mBackground); 2962 } 2963 else { 2964 mTextRenderer = new Bitmap4x8FontRenderer(getResources(), 2965 mForeground, mBackground); 2966 } 2967 mBackgroundPaint.setColor(mBackground); 2968 mCharacterWidth = mTextRenderer.getCharacterWidth(); 2969 mCharacterHeight = mTextRenderer.getCharacterHeight(); 2970 2971 if (mKnownSize) { 2972 updateSize(getWidth(), getHeight()); 2973 } 2974 } 2975 2976 @Override 2977 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 2978 updateSize(w, h); 2979 if (!mKnownSize) { 2980 mKnownSize = true; 2981 2982 // Set up a thread to read input from the 2983 // pseudo-teletype: 2984 2985 mPollingThread = new Thread(new Runnable() { 2986 2987 public void run() { 2988 try { 2989 while(true) { 2990 int read = mTermIn.read(mBuffer); 2991 mByteQueue.write(mBuffer, 0, read); 2992 mHandler.sendMessage( 2993 mHandler.obtainMessage(UPDATE)); 2994 } 2995 } catch (IOException e) { 2996 } catch (InterruptedException e) { 2997 } 2998 } 2999 private byte[] mBuffer = new byte[4096]; 3000 }); 3001 mPollingThread.setName("Input reader"); 3002 mPollingThread.start(); 3003 } 3004 } 3005 3006 private void updateSize(int w, int h) { 3007 mColumns = w / mCharacterWidth; 3008 mRows = h / mCharacterHeight; 3009 3010 // Inform the attached pty of our new size: 3011 Exec.setPtyWindowSize(mTermFd, mRows, mColumns, w, h); 3012 3013 3014 if (mTranscriptScreen != null) { 3015 mEmulator.updateSize(mColumns, mRows); 3016 } else { 3017 mTranscriptScreen = 3018 new TranscriptScreen(mColumns, TRANSCRIPT_ROWS, mRows, 0, 7); 3019 mEmulator = 3020 new TerminalEmulator(mTranscriptScreen, mColumns, mRows, 3021 mTermOut); 3022 } 3023 3024 // Reset our paging: 3025 mTopRow = 0; 3026 mLeftColumn = 0; 3027 3028 invalidate(); 3029 } 3030 3031 void updateSize() { 3032 if (mKnownSize) { 3033 updateSize(getWidth(), getHeight()); 3034 } 3035 } 3036 3037 /** 3038 * Look for new input from the ptty, send it to the terminal emulator. 3039 */ 3040 private void update() { 3041 int bytesAvailable = mByteQueue.getBytesAvailable(); 3042 int bytesToRead = Math.min(bytesAvailable, mReceiveBuffer.length); 3043 try { 3044 int bytesRead = mByteQueue.read(mReceiveBuffer, 0, bytesToRead); 3045 append(mReceiveBuffer, 0, bytesRead); 3046 } catch (InterruptedException e) { 3047 } 3048 } 3049 3050 @Override 3051 protected void onDraw(Canvas canvas) { 3052 int w = getWidth(); 3053 int h = getHeight(); 3054 canvas.drawRect(0, 0, w, h, mBackgroundPaint); 3055 mVisibleColumns = w / mCharacterWidth; 3056 float x = -mLeftColumn * mCharacterWidth; 3057 float y = mCharacterHeight; 3058 int endLine = mTopRow + mRows; 3059 int cx = mEmulator.getCursorCol(); 3060 int cy = mEmulator.getCursorRow(); 3061 for (int i = mTopRow; i < endLine; i++) { 3062 int cursorX = -1; 3063 if (i == cy) { 3064 cursorX = cx; 3065 } 3066 mTranscriptScreen.drawText(i, canvas, x, y, mTextRenderer, cursorX); 3067 y += mCharacterHeight; 3068 } 3069 } 3070 3071 private void ensureCursorVisible() { 3072 mTopRow = 0; 3073 if (mVisibleColumns > 0) { 3074 int cx = mEmulator.getCursorCol(); 3075 int visibleCursorX = mEmulator.getCursorCol() - mLeftColumn; 3076 if (visibleCursorX < 0) { 3077 mLeftColumn = cx; 3078 } else if (visibleCursorX >= mVisibleColumns) { 3079 mLeftColumn = (cx - mVisibleColumns) + 1; 3080 } 3081 } 3082 } 3083 } 3084 3085 3086 /** 3087 * An ASCII key listener. Supports control characters and escape. Keeps track of 3088 * the current state of the alt, shift, and control keys. 3089 */ 3090 class TermKeyListener { 3091 /** 3092 * The state engine for a modifier key. Can be pressed, released, locked, 3093 * and so on. 3094 * 3095 */ 3096 private class ModifierKey { 3097 3098 private int mState; 3099 3100 private static final int UNPRESSED = 0; 3101 3102 private static final int PRESSED = 1; 3103 3104 private static final int RELEASED = 2; 3105 3106 private static final int USED = 3; 3107 3108 private static final int LOCKED = 4; 3109 3110 /** 3111 * Construct a modifier key. UNPRESSED by default. 3112 * 3113 */ 3114 public ModifierKey() { 3115 mState = UNPRESSED; 3116 } 3117 3118 public void onPress() { 3119 switch (mState) { 3120 case PRESSED: 3121 // This is a repeat before use 3122 break; 3123 case RELEASED: 3124 mState = LOCKED; 3125 break; 3126 case USED: 3127 // This is a repeat after use 3128 break; 3129 case LOCKED: 3130 mState = UNPRESSED; 3131 break; 3132 default: 3133 mState = PRESSED; 3134 break; 3135 } 3136 } 3137 3138 public void onRelease() { 3139 switch (mState) { 3140 case USED: 3141 mState = UNPRESSED; 3142 break; 3143 case PRESSED: 3144 mState = RELEASED; 3145 break; 3146 default: 3147 // Leave state alone 3148 break; 3149 } 3150 } 3151 3152 public void adjustAfterKeypress() { 3153 switch (mState) { 3154 case PRESSED: 3155 mState = USED; 3156 break; 3157 case RELEASED: 3158 mState = UNPRESSED; 3159 break; 3160 default: 3161 // Leave state alone 3162 break; 3163 } 3164 } 3165 3166 public boolean isActive() { 3167 return mState != UNPRESSED; 3168 } 3169 } 3170 3171 private ModifierKey mAltKey = new ModifierKey(); 3172 3173 private ModifierKey mCapKey = new ModifierKey(); 3174 3175 private ModifierKey mControlKey = new ModifierKey(); 3176 3177 /** 3178 * Construct a term key listener. 3179 * 3180 */ 3181 public TermKeyListener() { 3182 } 3183 3184 public void handleControlKey(boolean down) { 3185 if (down) { 3186 mControlKey.onPress(); 3187 } else { 3188 mControlKey.onRelease(); 3189 } 3190 } 3191 3192 public int mapControlChar(int ch) { 3193 int result = ch; 3194 if (mControlKey.isActive()) { 3195 // Search is the control key. 3196 if (result >= 'a' && result <= 'z') { 3197 result = (char) (result - 'a' + '\001'); 3198 } else if (result == ' ') { 3199 result = 0; 3200 } else if ((result == '[') || (result == '1')) { 3201 result = 27; 3202 } else if ((result == '\\') || (result == '.')) { 3203 result = 28; 3204 } else if ((result == ']') || (result == '0')) { 3205 result = 29; 3206 } else if ((result == '^') || (result == '6')) { 3207 result = 30; // control-^ 3208 } else if ((result == '_') || (result == '5')) { 3209 result = 31; 3210 } 3211 } 3212 3213 if (result > -1) { 3214 mAltKey.adjustAfterKeypress(); 3215 mCapKey.adjustAfterKeypress(); 3216 mControlKey.adjustAfterKeypress(); 3217 } 3218 return result; 3219 } 3220 3221 /** 3222 * Handle a keyDown event. 3223 * 3224 * @param keyCode the keycode of the keyDown event 3225 * @return the ASCII byte to transmit to the pseudo-teletype, or -1 if this 3226 * event does not produce an ASCII byte. 3227 */ 3228 public int keyDown(int keyCode, KeyEvent event) { 3229 int result = -1; 3230 switch (keyCode) { 3231 case KeyEvent.KEYCODE_ALT_RIGHT: 3232 case KeyEvent.KEYCODE_ALT_LEFT: 3233 mAltKey.onPress(); 3234 break; 3235 3236 case KeyEvent.KEYCODE_SHIFT_LEFT: 3237 case KeyEvent.KEYCODE_SHIFT_RIGHT: 3238 mCapKey.onPress(); 3239 break; 3240 3241 case KeyEvent.KEYCODE_ENTER: 3242 // Convert newlines into returns. The vt100 sends a 3243 // '\r' when the 'Return' key is pressed, but our 3244 // KeyEvent translates this as a '\n'. 3245 result = '\r'; 3246 break; 3247 3248 case KeyEvent.KEYCODE_DEL: 3249 // Convert DEL into 127 (instead of 8) 3250 result = 127; 3251 break; 3252 3253 default: { 3254 result = event.getUnicodeChar( 3255 (mCapKey.isActive() ? KeyEvent.META_SHIFT_ON : 0) | 3256 (mAltKey.isActive() ? KeyEvent.META_ALT_ON : 0)); 3257 break; 3258 } 3259 } 3260 3261 result = mapControlChar(result); 3262 3263 return result; 3264 } 3265 3266 /** 3267 * Handle a keyUp event. 3268 * 3269 * @param keyCode the keyCode of the keyUp event 3270 */ 3271 public void keyUp(int keyCode) { 3272 switch (keyCode) { 3273 case KeyEvent.KEYCODE_ALT_LEFT: 3274 case KeyEvent.KEYCODE_ALT_RIGHT: 3275 mAltKey.onRelease(); 3276 break; 3277 case KeyEvent.KEYCODE_SHIFT_LEFT: 3278 case KeyEvent.KEYCODE_SHIFT_RIGHT: 3279 mCapKey.onRelease(); 3280 break; 3281 default: 3282 // Ignore other keyUps 3283 break; 3284 } 3285 } 3286 } 3287