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