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 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 181 public void onDestroy() { 182 super.onDestroy(); 183 if (mTermFd != null) { 184 Exec.close(mTermFd); 185 mTermFd = null; 186 } 187 } 188 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 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 231 private void restart() { 232 startActivity(getIntent()); 233 finish(); 234 } 235 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 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 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 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 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 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 362 private String readStringPref(String key, String defaultValue) { 363 return mPrefs.getString(key, defaultValue); 364 } 365 366 @Override 367 public void onResume() { 368 super.onResume(); 369 readPrefs(); 370 updatePrefs(); 371 } 372 373 @Override 374 public void onConfigurationChanged(Configuration newConfig) { 375 super.onConfigurationChanged(newConfig); 376 377 mEmulatorView.updateSize(); 378 } 379 380 @Override 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 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 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 */ 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 475 private boolean isSystemKey(int keyCode, KeyEvent event) { 476 return event.isSystem(); 477 } 478 479 @Override 480 public boolean onCreateOptionsMenu(Menu menu) { 481 getMenuInflater().inflate(R.menu.main, menu); 482 return true; 483 } 484 485 @Override 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 500 private void doPreferences() { 501 startActivity(new Intent(this, TermPreferences.class)); 502 } 503 504 private void setColors() { 505 int[] scheme = COLOR_SCHEMES[mColorId]; 506 mEmulatorView.setColors(scheme[0], scheme[1]); 507 } 508 509 private void doResetTerminal() { 510 restart(); 511 } 512 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 (at) 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 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 612 String getTranscriptText(); 613 614 /** 615 * Resize the screen 616 * @param columns 617 * @param rows 618 */ 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 */ 695 public TranscriptScreen(int columns, int totalRows, int screenRows, 696 int foreColor, int backColor) { 697 init(columns, totalRows, screenRows, foreColor, backColor); 698 } 699 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 */ 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 738 private int getOffset(int externalLine) { 739 return externalToInternalRow(externalLine) * mColumns; 740 } 741 742 private int getOffset(int x, int y) { 743 return getOffset(y) + x; 744 } 745 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 */ 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 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 */ 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 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 832 private void checkPositive(int n) { 833 if (n < 0) { 834 throw new IllegalArgumentException("checkPositive " + n); 835 } 836 } 837 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 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 */ 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 */ 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 */ 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 */ 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 */ 983 public int getActiveTranscriptRows() { 984 return mActiveTranscriptRows; 985 } 986 987 public String getTranscriptText() { 988 return internalGetTranscriptText(true); 989 } 990 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 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 */ 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 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 */ 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 */ 1317 public final int getCursorCol() { 1318 return mCursorCol; 1319 } 1320 1321 public final boolean getKeypadApplicationMode() { 1322 return mbKeypadApplicationMode; 1323 } 1324 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 */ 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 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 1459 private void setAltCharSet(boolean alternateCharSet) { 1460 mAlternateCharSet = alternateCharSet; 1461 } 1462 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 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 1511 private int getDecFlagsMask(int argument) { 1512 if (argument >= 1 && argument <= 9) { 1513 return (1 << argument); 1514 } 1515 1516 return 0; 1517 } 1518 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 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 1536 private void continueSequence() { 1537 mContinueSequence = true; 1538 } 1539 1540 private void continueSequence(int state) { 1541 mEscapeState = state; 1542 mContinueSequence = true; 1543 } 1544 1545 private void doEscSelectLeftParen(byte b) { 1546 doSelectCharSet(true, b); 1547 } 1548 1549 private void doEscSelectRightParen(byte b) { 1550 doSelectCharSet(false, b); 1551 } 1552 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 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 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 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 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 1895 private void blockClear(int sx, int sy, int w) { 1896 blockClear(sx, sy, w, 1); 1897 } 1898 1899 private void blockClear(int sx, int sy, int w, int h) { 1900 mScreen.blockSet(sx, sy, w, h, ' ', getForeColor(), getBackColor()); 1901 } 1902 1903 private int getForeColor() { 1904 return mInverseColors ? 1905 ((mBackColor & 0x7) | (mForeColor & 0x8)) : mForeColor; 1906 } 1907 1908 private int getBackColor() { 1909 return mInverseColors ? 1910 ((mForeColor & 0x7) | (mBackColor & 0x8)) : mBackColor; 1911 } 1912 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 1930 private void setHorizontalVerticalPosition() { 1931 1932 // Parameters are Row ; Column 1933 1934 setCursorPosition(getArg1(1) - 1, getArg0(1) - 1); 1935 } 1936 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 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 */ 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 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 */ 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 2026 private int getArg0(int defaultValue) { 2027 return getArg(0, defaultValue); 2028 } 2029 2030 private int getArg1(int defaultValue) { 2031 return getArg(1, defaultValue); 2032 } 2033 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 2042 private void unimplementedSequence(byte b) { 2043 if (Term.LOG_UNKNOWN_ESCAPE_SEQUENCES) { 2044 logError("unimplemented", b); 2045 } 2046 finishSequence(); 2047 } 2048 2049 private void unknownSequence(byte b) { 2050 if (Term.LOG_UNKNOWN_ESCAPE_SEQUENCES) { 2051 logError("unknown", b); 2052 } 2053 finishSequence(); 2054 } 2055 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 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 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 2099 private void finishSequence() { 2100 mEscapeState = ESC_NONE; 2101 } 2102 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 */ 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 2146 private void setCursorRow(int row) { 2147 mCursorRow = row; 2148 mAboutToAutoWrap = false; 2149 } 2150 2151 private void setCursorCol(int col) { 2152 mCursorCol = col; 2153 mAboutToAutoWrap = false; 2154 } 2155 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 */ 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 2190 public String getTranscriptText() { 2191 return mScreen.getTranscriptText(); 2192 } 2193 } 2194 2195 /** 2196 * Text renderer interface 2197 */ 2198 2199 interface TextRenderer { 2200 int getCharacterWidth(); 2201 int getCharacterHeight(); 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 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 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 2256 public int getCharacterWidth() { 2257 return kCharacterWidth; 2258 } 2259 2260 public int getCharacterHeight() { 2261 return kCharacterHeight; 2262 } 2263 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 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 { 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 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 2359 public int getCharacterHeight() { 2360 return mCharHeight; 2361 } 2362 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 { 2382 public ByteQueue(int size) { 2383 mBuffer = new byte[size]; 2384 } 2385 2386 public int getBytesAvailable() { 2387 synchronized(this) { 2388 return mStoredBytes; 2389 } 2390 } 2391 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 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 2615 public EmulatorView(Context context) { 2616 super(context); 2617 commonConstructor(); 2618 } 2619 2620 public void register(TermKeyListener listener) { 2621 mKeyListener = listener; 2622 } 2623 2624 public void setColors(int foreground, int background) { 2625 mForeground = foreground; 2626 mBackground = background; 2627 updateText(); 2628 } 2629 2630 public String getTranscriptText() { 2631 return mEmulator.getTranscriptText(); 2632 } 2633 2634 public void resetTerminal() { 2635 mEmulator.reset(); 2636 invalidate(); 2637 } 2638 2639 @Override 2640 public boolean onCheckIsTextEditor() { 2641 return true; 2642 } 2643 2644 @Override 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(); 2788 } 2789 2790 private void commonConstructor() { 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(this); 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