/* * Copyright (C) 2007 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.term; import android.app.Activity; import android.app.AlertDialog; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.Configuration; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.ColorMatrixColorFilter; import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.graphics.Rect; import android.graphics.Typeface; import android.net.Uri; import android.os.Bundle; import android.os.Exec; import android.os.Handler; import android.os.Message; import android.os.SystemClock; import android.preference.PreferenceManager; import android.util.AttributeSet; import android.util.Log; import android.view.GestureDetector; import android.view.KeyEvent; import android.view.Menu; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.inputmethod.BaseInputConnection; import android.view.inputmethod.CompletionInfo; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.ExtractedText; import android.view.inputmethod.ExtractedTextRequest; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; import java.io.FileDescriptor; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.util.ArrayList; /** * A terminal emulator activity. */ public class Term extends Activity { /** * Set to true to add debugging code and logging. */ public static final boolean DEBUG = false; /** * Set to true to log each character received from the remote process to the * android log, which makes it easier to debug some kinds of problems with * emulating escape sequences and control codes. */ public static final boolean LOG_CHARACTERS_FLAG = DEBUG && false; /** * Set to true to log unknown escape sequences. */ public static final boolean LOG_UNKNOWN_ESCAPE_SEQUENCES = DEBUG && false; /** * The tag we use when logging, so that our messages can be distinguished * from other messages in the log. Public because it's used by several * classes. */ public static final String LOG_TAG = "Term"; /** * Our main view. Displays the emulated terminal screen. */ private EmulatorView mEmulatorView; /** * The pseudo-teletype (pty) file descriptor that we use to communicate with * another process, typically a shell. Currently we just use this to get the * mTermIn / mTermOut file descriptors, but when we implement resizing of * the terminal we will need it to issue the ioctl to inform the other * process that we've changed the terminal size. */ private FileDescriptor mTermFd; private boolean mShellRunning; /** * Used to send data to the remote process. */ private FileOutputStream mTermOut; /** * A key listener that tracks the modifier keys and allows the full ASCII * character set to be entered. */ private TermKeyListener mKeyListener; /** * The name of our emulator view in the view resource. */ private static final int EMULATOR_VIEW = R.id.emulatorView; private int mFontSize = 9; private int mColorId = 2; private int mControlKeyId = 0; private static final String FONTSIZE_KEY = "fontsize"; private static final String COLOR_KEY = "color"; private static final String CONTROLKEY_KEY = "controlkey"; private static final String SHELL_KEY = "shell"; private static final String INITIALCOMMAND_KEY = "initialcommand"; public static final int WHITE = 0xffffffff; public static final int BLACK = 0xff000000; public static final int BLUE = 0xff344ebd; private static final int[][] COLOR_SCHEMES = { {BLACK, WHITE}, {WHITE, BLACK}, {WHITE, BLUE}}; private static final int[] CONTROL_KEY_SCHEMES = { KeyEvent.KEYCODE_DPAD_CENTER, KeyEvent.KEYCODE_AT, KeyEvent.KEYCODE_ALT_LEFT, KeyEvent.KEYCODE_ALT_RIGHT }; private static final String[] CONTROL_KEY_NAME = { "Ball", "@", "Left-Alt", "Right-Alt" }; private int mControlKeyCode; private final static String DEFAULT_SHELL = "/system/bin/sh -"; private String mShell; private final static String DEFAULT_INITIAL_COMMAND = "export PATH=/data/local/bin:$PATH"; private String mInitialCommand; private SharedPreferences mPrefs; @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); Log.e(Term.LOG_TAG, "onCreate"); mPrefs = PreferenceManager.getDefaultSharedPreferences(this); mPrefs.registerOnSharedPreferenceChangeListener( new SharedPreferences.OnSharedPreferenceChangeListener(){ public void onSharedPreferenceChanged( SharedPreferences sharedPreferences, String key) { readPrefs(); updatePrefs(); }}); readPrefs(); setContentView(R.layout.term_activity); mEmulatorView = (EmulatorView) findViewById(EMULATOR_VIEW); startListening(); mKeyListener = new TermKeyListener(); mEmulatorView.setFocusable(true); mEmulatorView.requestFocus(); updatePrefs(); } private void startListening() { int[] processId = new int[1]; createSubprocess(processId); mShellRunning = true; final int procId = processId[0]; final Handler handler = new Handler() { @Override public void handleMessage(Message msg) { mShellRunning = false; } }; Runnable watchForDeath = new Runnable() { public void run() { Log.i(Term.LOG_TAG, "waiting for: " + procId); int result = Exec.waitFor(procId); Log.i(Term.LOG_TAG, "Subprocess exited: " + result); handler.sendEmptyMessage(result); } }; Thread watcher = new Thread(watchForDeath); watcher.start(); mTermOut = new FileOutputStream(mTermFd); mEmulatorView.initialize(mTermFd, mTermOut); sendInitialCommand(); } private void sendInitialCommand() { String initialCommand = mInitialCommand; if (initialCommand == null) { initialCommand = DEFAULT_INITIAL_COMMAND; } if (initialCommand.length() > 0) { write(initialCommand + '\r'); } } private void restart() { startActivity(getIntent()); finish(); } private void write(String data) { try { mTermOut.write(data.getBytes()); mTermOut.flush(); } catch (IOException e) { // Ignore exception // We don't really care if the receiver isn't listening. // We just make a best effort to answer the query. } } private void createSubprocess(int[] processId) { String shell = mShell; if (shell == null) { shell = DEFAULT_SHELL; } ArrayList<String> args = parse(shell); String arg0 = args.get(0); String arg1 = null; String arg2 = null; if (args.size() >= 2) { arg1 = args.get(1); } if (args.size() >= 3) { arg2 = args.get(2); } mTermFd = Exec.createSubprocess(arg0, arg1, arg2, processId); } private ArrayList<String> parse(String cmd) { final int PLAIN = 0; final int WHITESPACE = 1; final int INQUOTE = 2; int state = WHITESPACE; ArrayList<String> result = new ArrayList<String>(); int cmdLen = cmd.length(); StringBuilder builder = new StringBuilder(); for (int i = 0; i < cmdLen; i++) { char c = cmd.charAt(i); if (state == PLAIN) { if (Character.isWhitespace(c)) { result.add(builder.toString()); builder.delete(0,builder.length()); state = WHITESPACE; } else if (c == '"') { state = INQUOTE; } else { builder.append(c); } } else if (state == WHITESPACE) { if (Character.isWhitespace(c)) { // do nothing } else if (c == '"') { state = INQUOTE; } else { state = PLAIN; builder.append(c); } } else if (state == INQUOTE) { if (c == '\\') { if (i + 1 < cmdLen) { i += 1; builder.append(cmd.charAt(i)); } } else if (c == '"') { state = PLAIN; } else { builder.append(c); } } } if (builder.length() > 0) { result.add(builder.toString()); } return result; } private void readPrefs() { mFontSize = readIntPref(FONTSIZE_KEY, mFontSize, 20); mColorId = readIntPref(COLOR_KEY, mColorId, COLOR_SCHEMES.length - 1); mControlKeyId = readIntPref(CONTROLKEY_KEY, mControlKeyId, CONTROL_KEY_SCHEMES.length - 1); { String newShell = readStringPref(SHELL_KEY, mShell); if ((newShell == null) || ! newShell.equals(mShell)) { if (mShell != null) { Log.i(Term.LOG_TAG, "New shell set. Restarting."); restart(); } mShell = newShell; } } { String newInitialCommand = readStringPref(INITIALCOMMAND_KEY, mInitialCommand); if ((newInitialCommand == null) || ! newInitialCommand.equals(mInitialCommand)) { if (mInitialCommand != null) { Log.i(Term.LOG_TAG, "New initial command set. Restarting."); restart(); } mInitialCommand = newInitialCommand; } } } private void updatePrefs() { mEmulatorView.setTextSize(mFontSize); setColors(); mControlKeyCode = CONTROL_KEY_SCHEMES[mControlKeyId]; } private int readIntPref(String key, int defaultValue, int maxValue) { int val; try { val = Integer.parseInt( mPrefs.getString(key, Integer.toString(defaultValue))); } catch (NumberFormatException e) { val = defaultValue; } val = Math.max(0, Math.min(val, maxValue)); return val; } private String readStringPref(String key, String defaultValue) { return mPrefs.getString(key, defaultValue); } @Override public void onPause() { SharedPreferences.Editor e = mPrefs.edit(); e.clear(); e.putString(FONTSIZE_KEY, Integer.toString(mFontSize)); e.putString(COLOR_KEY, Integer.toString(mColorId)); e.putString(CONTROLKEY_KEY, Integer.toString(mControlKeyId)); e.putString(SHELL_KEY, mShell); e.putString(INITIALCOMMAND_KEY, mInitialCommand); e.commit(); super.onPause(); } @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); mEmulatorView.updateSize(); } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if (handleControlKey(keyCode, true)) { return true; } else if (isSystemKey(keyCode, event)) { // Don't intercept the system keys return super.onKeyDown(keyCode, event); } else if (handleDPad(keyCode, true)) { return true; } // Translate the keyCode into an ASCII character. int letter = mKeyListener.keyDown(keyCode, event); if (letter >= 0) { try { mTermOut.write(letter); } catch (IOException e) { // Ignore I/O exceptions } } return true; } @Override public boolean onKeyUp(int keyCode, KeyEvent event) { if (handleControlKey(keyCode, false)) { return true; } else if (isSystemKey(keyCode, event)) { // Don't intercept the system keys return super.onKeyUp(keyCode, event); } else if (handleDPad(keyCode, false)) { return true; } mKeyListener.keyUp(keyCode); return true; } private boolean handleControlKey(int keyCode, boolean down) { if (keyCode == mControlKeyCode) { mKeyListener.handleControlKey(down); return true; } return false; } /** * Handle dpad left-right-up-down events. Don't handle * dpad-center, that's our control key. * @param keyCode * @param down */ private boolean handleDPad(int keyCode, boolean down) { if (keyCode < KeyEvent.KEYCODE_DPAD_UP || keyCode > KeyEvent.KEYCODE_DPAD_CENTER) { return false; } if (down) { try { if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) { mTermOut.write('\r'); } else { char code; switch (keyCode) { case KeyEvent.KEYCODE_DPAD_UP: code = 'A'; break; case KeyEvent.KEYCODE_DPAD_DOWN: code = 'B'; break; case KeyEvent.KEYCODE_DPAD_LEFT: code = 'D'; break; default: case KeyEvent.KEYCODE_DPAD_RIGHT: code = 'C'; break; } mTermOut.write(27); // ESC if (mEmulatorView.getKeypadApplicationMode()) { mTermOut.write('O'); } else { mTermOut.write('['); } mTermOut.write(code); } } catch (IOException e) { // Ignore } } return true; } private boolean isSystemKey(int keyCode, KeyEvent event) { return event.isSystem(); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.main, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); if (id == R.id.menu_preferences) { doPreferences(); } else if (id == R.id.menu_reset) { doResetTerminal(); } else if (id == R.id.menu_send_email) { doEmailTranscript(); } else if (id == R.id.menu_special_keys) { doDocumentKeys(); } return super.onOptionsItemSelected(item); } private void doPreferences() { startActivity(new Intent(this, TermPreferences.class)); } private void setColors() { int[] scheme = COLOR_SCHEMES[mColorId]; mEmulatorView.setColors(scheme[0], scheme[1]); } private void doResetTerminal() { restart(); } private void doEmailTranscript() { // Don't really want to supply an address, but // currently it's required, otherwise we get an // exception. String addr = "user@example.com"; Intent intent = new Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:" + addr)); intent.putExtra("body", mEmulatorView.getTranscriptText()); startActivity(intent); } private void doDocumentKeys() { String controlKey = CONTROL_KEY_NAME[mControlKeyId]; new AlertDialog.Builder(this). setTitle("Press " + controlKey + " and Key"). setMessage(controlKey + " Space ==> Control-@ (NUL)\n" + controlKey + " A..Z ==> Control-A..Z\n" + controlKey + " 1 ==> Control-[ (ESC)\n" + controlKey + " 5 ==> Control-_\n" + controlKey + " . ==> Control-\\\n" + controlKey + " 0 ==> Control-]\n" + controlKey + " 6 ==> Control-^"). show(); } } /** * An abstract screen interface. A terminal screen stores lines of text. (The * reason to abstract it is to allow different implementations, and to hide * implementation details from clients.) */ interface Screen { /** * Set line wrap flag for a given row. Affects how lines are logically * wrapped when changing screen size or converting to a transcript. */ void setLineWrap(int row); /** * Store byte b into the screen at location (x, y) * * @param x X coordinate (also known as column) * @param y Y coordinate (also known as row) * @param b ASCII character to store * @param foreColor the foreground color * @param backColor the background color */ void set(int x, int y, byte b, int foreColor, int backColor); /** * Scroll the screen down one line. To scroll the whole screen of a 24 line * screen, the arguments would be (0, 24). * * @param topMargin First line that is scrolled. * @param bottomMargin One line after the last line that is scrolled. */ void scroll(int topMargin, int bottomMargin, int foreColor, int backColor); /** * Block copy characters from one position in the screen to another. The two * positions can overlap. All characters of the source and destination must * be within the bounds of the screen, or else an InvalidParemeterException * will be thrown. * * @param sx source X coordinate * @param sy source Y coordinate * @param w width * @param h height * @param dx destination X coordinate * @param dy destination Y coordinate */ void blockCopy(int sx, int sy, int w, int h, int dx, int dy); /** * Block set characters. All characters must be within the bounds of the * screen, or else and InvalidParemeterException will be thrown. Typically * this is called with a "val" argument of 32 to clear a block of * characters. * * @param sx source X * @param sy source Y * @param w width * @param h height * @param val value to set. * @param foreColor the foreground color * @param backColor the background color */ void blockSet(int sx, int sy, int w, int h, int val, int foreColor, int backColor); /** * Get the contents of the transcript buffer as a text string. * * @return the contents of the transcript buffer. */ String getTranscriptText(); /** * Resize the screen * @param columns * @param rows */ void resize(int columns, int rows, int foreColor, int backColor); } /** * A TranscriptScreen is a screen that remembers data that's been scrolled. The * old data is stored in a ring buffer to minimize the amount of copying that * needs to be done. The transcript does its own drawing, to avoid having to * expose its internal data structures. */ class TranscriptScreen implements Screen { /** * The width of the transcript, in characters. Fixed at initialization. */ private int mColumns; /** * The total number of rows in the transcript and the screen. Fixed at * initialization. */ private int mTotalRows; /** * The number of rows in the active portion of the transcript. Doesn't * include the screen. */ private int mActiveTranscriptRows; /** * Which row is currently the topmost line of the transcript. Used to * implement a circular buffer. */ private int mHead; /** * The number of active rows, includes both the transcript and the screen. */ private int mActiveRows; /** * The number of rows in the screen. */ private int mScreenRows; /** * The data for both the screen and the transcript. The first mScreenRows * * mLineWidth characters are the screen, the rest are the transcript. * The low byte encodes the ASCII character, the high byte encodes the * foreground and background colors, plus underline and bold. */ private char[] mData; /** * The data's stored as color-encoded chars, but the drawing routines require chars, so we * need a temporary buffer to hold a row's worth of characters. */ private char[] mRowBuffer; /** * Flags that keep track of whether the current line logically wraps to the * next line. This is used when resizing the screen and when copying to the * clipboard or an email attachment */ private boolean[] mLineWrap; /** * Create a transcript screen. * * @param columns the width of the screen in characters. * @param totalRows the height of the entire text area, in rows of text. * @param screenRows the height of just the screen, not including the * transcript that holds lines that have scrolled off the top of the * screen. */ public TranscriptScreen(int columns, int totalRows, int screenRows, int foreColor, int backColor) { init(columns, totalRows, screenRows, foreColor, backColor); } private void init(int columns, int totalRows, int screenRows, int foreColor, int backColor) { mColumns = columns; mTotalRows = totalRows; mActiveTranscriptRows = 0; mHead = 0; mActiveRows = screenRows; mScreenRows = screenRows; int totalSize = columns * totalRows; mData = new char[totalSize]; blockSet(0, 0, mColumns, mScreenRows, ' ', foreColor, backColor); mRowBuffer = new char[columns]; mLineWrap = new boolean[totalRows]; consistencyCheck(); } /** * Convert a row value from the public external coordinate system to our * internal private coordinate system. External coordinate system: * -mActiveTranscriptRows to mScreenRows-1, with the screen being * 0..mScreenRows-1 Internal coordinate system: 0..mScreenRows-1 rows of * mData are the visible rows. mScreenRows..mActiveRows - 1 are the * transcript, stored as a circular buffer. * * @param row a row in the external coordinate system. * @return The row corresponding to the input argument in the private * coordinate system. */ private int externalToInternalRow(int row) { if (row < -mActiveTranscriptRows || row >= mScreenRows) { throw new IllegalArgumentException(); } if (row >= 0) { return row; // This is a visible row. } return mScreenRows + ((mHead + mActiveTranscriptRows + row) % mActiveTranscriptRows); } private int getOffset(int externalLine) { return externalToInternalRow(externalLine) * mColumns; } private int getOffset(int x, int y) { return getOffset(y) + x; } public void setLineWrap(int row) { mLineWrap[externalToInternalRow(row)] = true; } /** * Store byte b into the screen at location (x, y) * * @param x X coordinate (also known as column) * @param y Y coordinate (also known as row) * @param b ASCII character to store * @param foreColor the foreground color * @param backColor the background color */ public void set(int x, int y, byte b, int foreColor, int backColor) { mData[getOffset(x, y)] = encode(b, foreColor, backColor); } private char encode(int b, int foreColor, int backColor) { return (char) ((foreColor << 12) | (backColor << 8) | b); } /** * Scroll the screen down one line. To scroll the whole screen of a 24 line * screen, the arguments would be (0, 24). * * @param topMargin First line that is scrolled. * @param bottomMargin One line after the last line that is scrolled. */ public void scroll(int topMargin, int bottomMargin, int foreColor, int backColor) { if (topMargin > bottomMargin - 2 || topMargin > mScreenRows - 2 || bottomMargin > mScreenRows) { throw new IllegalArgumentException(); } // Adjust the transcript so that the last line of the transcript // is ready to receive the newly scrolled data consistencyCheck(); int expansionRows = Math.min(1, mTotalRows - mActiveRows); int rollRows = 1 - expansionRows; mActiveRows += expansionRows; mActiveTranscriptRows += expansionRows; if (mActiveTranscriptRows > 0) { mHead = (mHead + rollRows) % mActiveTranscriptRows; } consistencyCheck(); // Block move the scroll line to the transcript int topOffset = getOffset(topMargin); int destOffset = getOffset(-1); System.arraycopy(mData, topOffset, mData, destOffset, mColumns); int topLine = externalToInternalRow(topMargin); int destLine = externalToInternalRow(-1); System.arraycopy(mLineWrap, topLine, mLineWrap, destLine, 1); // Block move the scrolled data up int numScrollChars = (bottomMargin - topMargin - 1) * mColumns; System.arraycopy(mData, topOffset + mColumns, mData, topOffset, numScrollChars); int numScrollLines = (bottomMargin - topMargin - 1); System.arraycopy(mLineWrap, topLine + 1, mLineWrap, topLine, numScrollLines); // Erase the bottom line of the scroll region blockSet(0, bottomMargin - 1, mColumns, 1, ' ', foreColor, backColor); mLineWrap[externalToInternalRow(bottomMargin-1)] = false; } private void consistencyCheck() { checkPositive(mColumns); checkPositive(mTotalRows); checkRange(0, mActiveTranscriptRows, mTotalRows); if (mActiveTranscriptRows == 0) { checkEqual(mHead, 0); } else { checkRange(0, mHead, mActiveTranscriptRows-1); } checkEqual(mScreenRows + mActiveTranscriptRows, mActiveRows); checkRange(0, mScreenRows, mTotalRows); checkEqual(mTotalRows, mLineWrap.length); checkEqual(mTotalRows*mColumns, mData.length); checkEqual(mColumns, mRowBuffer.length); } private void checkPositive(int n) { if (n < 0) { throw new IllegalArgumentException("checkPositive " + n); } } private void checkRange(int a, int b, int c) { if (a > b || b > c) { throw new IllegalArgumentException("checkRange " + a + " <= " + b + " <= " + c); } } private void checkEqual(int a, int b) { if (a != b) { throw new IllegalArgumentException("checkEqual " + a + " == " + b); } } /** * Block copy characters from one position in the screen to another. The two * positions can overlap. All characters of the source and destination must * be within the bounds of the screen, or else an InvalidParemeterException * will be thrown. * * @param sx source X coordinate * @param sy source Y coordinate * @param w width * @param h height * @param dx destination X coordinate * @param dy destination Y coordinate */ public void blockCopy(int sx, int sy, int w, int h, int dx, int dy) { if (sx < 0 || sx + w > mColumns || sy < 0 || sy + h > mScreenRows || dx < 0 || dx + w > mColumns || dy < 0 || dy + h > mScreenRows) { throw new IllegalArgumentException(); } if (sy <= dy) { // Move in increasing order for (int y = 0; y < h; y++) { int srcOffset = getOffset(sx, sy + y); int dstOffset = getOffset(dx, dy + y); System.arraycopy(mData, srcOffset, mData, dstOffset, w); } } else { // Move in decreasing order for (int y = 0; y < h; y++) { int y2 = h - (y + 1); int srcOffset = getOffset(sx, sy + y2); int dstOffset = getOffset(dx, dy + y2); System.arraycopy(mData, srcOffset, mData, dstOffset, w); } } } /** * Block set characters. All characters must be within the bounds of the * screen, or else and InvalidParemeterException will be thrown. Typically * this is called with a "val" argument of 32 to clear a block of * characters. * * @param sx source X * @param sy source Y * @param w width * @param h height * @param val value to set. */ public void blockSet(int sx, int sy, int w, int h, int val, int foreColor, int backColor) { if (sx < 0 || sx + w > mColumns || sy < 0 || sy + h > mScreenRows) { throw new IllegalArgumentException(); } char[] data = mData; char encodedVal = encode(val, foreColor, backColor); for (int y = 0; y < h; y++) { int offset = getOffset(sx, sy + y); for (int x = 0; x < w; x++) { data[offset + x] = encodedVal; } } } /** * Draw a row of text. Out-of-bounds rows are blank, not errors. * * @param row The row of text to draw. * @param canvas The canvas to draw to. * @param x The x coordinate origin of the drawing * @param y The y coordinate origin of the drawing * @param renderer The renderer to use to draw the text * @param cx the cursor X coordinate, -1 means don't draw it */ public final void drawText(int row, Canvas canvas, float x, float y, TextRenderer renderer, int cx) { // Out-of-bounds rows are blank. if (row < -mActiveTranscriptRows || row >= mScreenRows) { return; } // Copy the data from the byte array to a char array so they can // be drawn. int offset = getOffset(row); char[] rowBuffer = mRowBuffer; char[] data = mData; int columns = mColumns; int lastColors = 0; int lastRunStart = -1; final int CURSOR_MASK = 0x10000; for (int i = 0; i < columns; i++) { char c = data[offset + i]; int colors = (char) (c & 0xff00); if (cx == i) { // Set cursor background color: colors |= CURSOR_MASK; } rowBuffer[i] = (char) (c & 0x00ff); if (colors != lastColors) { if (lastRunStart >= 0) { renderer.drawTextRun(canvas, x, y, lastRunStart, rowBuffer, lastRunStart, i - lastRunStart, (lastColors & CURSOR_MASK) != 0, 0xf & (lastColors >> 12), 0xf & (lastColors >> 8)); } lastColors = colors; lastRunStart = i; } } if (lastRunStart >= 0) { renderer.drawTextRun(canvas, x, y, lastRunStart, rowBuffer, lastRunStart, columns - lastRunStart, (lastColors & CURSOR_MASK) != 0, 0xf & (lastColors >> 12), 0xf & (lastColors >> 8)); } } /** * Get the count of active rows. * * @return the count of active rows. */ public int getActiveRows() { return mActiveRows; } /** * Get the count of active transcript rows. * * @return the count of active transcript rows. */ public int getActiveTranscriptRows() { return mActiveTranscriptRows; } public String getTranscriptText() { return internalGetTranscriptText(true); } private String internalGetTranscriptText(boolean stripColors) { StringBuilder builder = new StringBuilder(); char[] rowBuffer = mRowBuffer; char[] data = mData; int columns = mColumns; for (int row = -mActiveTranscriptRows; row < mScreenRows; row++) { int offset = getOffset(row); int lastPrintingChar = -1; for (int column = 0; column < columns; column++) { char c = data[offset + column]; if (stripColors) { c = (char) (c & 0xff); } if ((c & 0xff) != ' ') { lastPrintingChar = column; } rowBuffer[column] = c; } if (mLineWrap[externalToInternalRow(row)]) { builder.append(rowBuffer, 0, columns); } else { builder.append(rowBuffer, 0, lastPrintingChar + 1); builder.append('\n'); } } return builder.toString(); } public void resize(int columns, int rows, int foreColor, int backColor) { init(columns, mTotalRows, rows, foreColor, backColor); } } /** * Renders text into a screen. Contains all the terminal-specific knowlege and * state. Emulates a subset of the X Window System xterm terminal, which in turn * is an emulator for a subset of the Digital Equipment Corporation vt100 * terminal. Missing functionality: text attributes (bold, underline, reverse * video, color) alternate screen cursor key and keypad escape sequences. */ class TerminalEmulator { /** * The cursor row. Numbered 0..mRows-1. */ private int mCursorRow; /** * The cursor column. Numbered 0..mColumns-1. */ private int mCursorCol; /** * The number of character rows in the terminal screen. */ private int mRows; /** * The number of character columns in the terminal screen. */ private int mColumns; /** * Used to send data to the remote process. Needed to implement the various * "report" escape sequences. */ private FileOutputStream mTermOut; /** * Stores the characters that appear on the screen of the emulated terminal. */ private Screen mScreen; /** * Keeps track of the current argument of the current escape sequence. * Ranges from 0 to MAX_ESCAPE_PARAMETERS-1. (Typically just 0 or 1.) */ private int mArgIndex; /** * The number of parameter arguments. This name comes from the ANSI standard * for terminal escape codes. */ private static final int MAX_ESCAPE_PARAMETERS = 16; /** * Holds the arguments of the current escape sequence. */ private int[] mArgs = new int[MAX_ESCAPE_PARAMETERS]; // Escape processing states: /** * Escape processing state: Not currently in an escape sequence. */ private static final int ESC_NONE = 0; /** * Escape processing state: Have seen an ESC character */ private static final int ESC = 1; /** * Escape processing state: Have seen ESC POUND */ private static final int ESC_POUND = 2; /** * Escape processing state: Have seen ESC and a character-set-select char */ private static final int ESC_SELECT_LEFT_PAREN = 3; /** * Escape processing state: Have seen ESC and a character-set-select char */ private static final int ESC_SELECT_RIGHT_PAREN = 4; /** * Escape processing state: ESC [ */ private static final int ESC_LEFT_SQUARE_BRACKET = 5; /** * Escape processing state: ESC [ ? */ private static final int ESC_LEFT_SQUARE_BRACKET_QUESTION_MARK = 6; /** * True if the current escape sequence should continue, false if the current * escape sequence should be terminated. Used when parsing a single * character. */ private boolean mContinueSequence; /** * The current state of the escape sequence state machine. */ private int mEscapeState; /** * Saved state of the cursor row, Used to implement the save/restore cursor * position escape sequences. */ private int mSavedCursorRow; /** * Saved state of the cursor column, Used to implement the save/restore * cursor position escape sequences. */ private int mSavedCursorCol; // DecSet booleans /** * This mask indicates 132-column mode is set. (As opposed to 80-column * mode.) */ private static final int K_132_COLUMN_MODE_MASK = 1 << 3; /** * This mask indicates that origin mode is set. (Cursor addressing is * relative to the absolute screen size, rather than the currently set top * and bottom margins.) */ private static final int K_ORIGIN_MODE_MASK = 1 << 6; /** * This mask indicates that wraparound mode is set. (As opposed to * stop-at-right-column mode.) */ private static final int K_WRAPAROUND_MODE_MASK = 1 << 7; /** * Holds multiple DECSET flags. The data is stored this way, rather than in * separate booleans, to make it easier to implement the save-and-restore * semantics. The various k*ModeMask masks can be used to extract and modify * the individual flags current states. */ private int mDecFlags; /** * Saves away a snapshot of the DECSET flags. Used to implement save and * restore escape sequences. */ private int mSavedDecFlags; // Modes set with Set Mode / Reset Mode /** * True if insert mode (as opposed to replace mode) is active. In insert * mode new characters are inserted, pushing existing text to the right. */ private boolean mInsertMode; /** * Automatic newline mode. Configures whether pressing return on the * keyboard automatically generates a return as well. Not currently * implemented. */ private boolean mAutomaticNewlineMode; /** * An array of tab stops. mTabStop[i] is true if there is a tab stop set for * column i. */ private boolean[] mTabStop; // The margins allow portions of the screen to be locked. /** * The top margin of the screen, for scrolling purposes. Ranges from 0 to * mRows-2. */ private int mTopMargin; /** * The bottom margin of the screen, for scrolling purposes. Ranges from * mTopMargin + 2 to mRows. (Defines the first row after the scrolling * region. */ private int mBottomMargin; /** * True if the next character to be emitted will be automatically wrapped to * the next line. Used to disambiguate the case where the cursor is * positioned on column mColumns-1. */ private boolean mAboutToAutoWrap; /** * Used for debugging, counts how many chars have been processed. */ private int mProcessedCharCount; /** * Foreground color, 0..7, mask with 8 for bold */ private int mForeColor; /** * Background color, 0..7, mask with 8 for underline */ private int mBackColor; private boolean mInverseColors; private boolean mbKeypadApplicationMode; private boolean mAlternateCharSet; /** * Construct a terminal emulator that uses the supplied screen * * @param screen the screen to render characters into. * @param columns the number of columns to emulate * @param rows the number of rows to emulate * @param termOut the output file descriptor that talks to the pseudo-tty. */ public TerminalEmulator(Screen screen, int columns, int rows, FileOutputStream termOut) { mScreen = screen; mRows = rows; mColumns = columns; mTabStop = new boolean[mColumns]; mTermOut = termOut; reset(); } public void updateSize(int columns, int rows) { if (mRows == rows && mColumns == columns) { return; } String transcriptText = mScreen.getTranscriptText(); mScreen.resize(columns, rows, mForeColor, mBackColor); if (mRows != rows) { mRows = rows; mTopMargin = 0; mBottomMargin = mRows; } if (mColumns != columns) { int oldColumns = mColumns; mColumns = columns; boolean[] oldTabStop = mTabStop; mTabStop = new boolean[mColumns]; int toTransfer = Math.min(oldColumns, columns); System.arraycopy(oldTabStop, 0, mTabStop, 0, toTransfer); while (mCursorCol >= columns) { mCursorCol -= columns; mCursorRow = Math.min(mBottomMargin-1, mCursorRow + 1); } } mCursorRow = 0; mCursorCol = 0; mAboutToAutoWrap = false; int end = transcriptText.length()-1; while ((end >= 0) && transcriptText.charAt(end) == '\n') { end--; } for(int i = 0; i <= end; i++) { byte c = (byte) transcriptText.charAt(i); if (c == '\n') { setCursorCol(0); doLinefeed(); } else { emit(c); } } } /** * Get the cursor's current row. * * @return the cursor's current row. */ public final int getCursorRow() { return mCursorRow; } /** * Get the cursor's current column. * * @return the cursor's current column. */ public final int getCursorCol() { return mCursorCol; } public final boolean getKeypadApplicationMode() { return mbKeypadApplicationMode; } private void setDefaultTabStops() { for (int i = 0; i < mColumns; i++) { mTabStop[i] = (i & 7) == 0 && i != 0; } } /** * Accept bytes (typically from the pseudo-teletype) and process them. * * @param buffer a byte array containing the bytes to be processed * @param base the first index of the array to process * @param length the number of bytes in the array to process */ public void append(byte[] buffer, int base, int length) { for (int i = 0; i < length; i++) { byte b = buffer[base + i]; try { if (Term.LOG_CHARACTERS_FLAG) { char printableB = (char) b; if (b < 32 || b > 126) { printableB = ' '; } Log.w(Term.LOG_TAG, "'" + Character.toString(printableB) + "' (" + Integer.toString((int) b) + ")"); } process(b); mProcessedCharCount++; } catch (Exception e) { Log.e(Term.LOG_TAG, "Exception while processing character " + Integer.toString(mProcessedCharCount) + " code " + Integer.toString(b), e); } } } private void process(byte b) { switch (b) { case 0: // NUL // Do nothing break; case 7: // BEL // Do nothing break; case 8: // BS setCursorCol(Math.max(0, mCursorCol - 1)); break; case 9: // HT // Move to next tab stop, but not past edge of screen setCursorCol(nextTabStop(mCursorCol)); break; case 13: setCursorCol(0); break; case 10: // CR case 11: // VT case 12: // LF doLinefeed(); break; case 14: // SO: setAltCharSet(true); break; case 15: // SI: setAltCharSet(false); break; case 24: // CAN case 26: // SUB if (mEscapeState != ESC_NONE) { mEscapeState = ESC_NONE; emit((byte) 127); } break; case 27: // ESC // Always starts an escape sequence startEscapeSequence(ESC); break; case (byte) 0x9b: // CSI startEscapeSequence(ESC_LEFT_SQUARE_BRACKET); break; default: mContinueSequence = false; switch (mEscapeState) { case ESC_NONE: if (b >= 32) { emit(b); } break; case ESC: doEsc(b); break; case ESC_POUND: doEscPound(b); break; case ESC_SELECT_LEFT_PAREN: doEscSelectLeftParen(b); break; case ESC_SELECT_RIGHT_PAREN: doEscSelectRightParen(b); break; case ESC_LEFT_SQUARE_BRACKET: doEscLeftSquareBracket(b); break; case ESC_LEFT_SQUARE_BRACKET_QUESTION_MARK: doEscLSBQuest(b); break; default: unknownSequence(b); break; } if (!mContinueSequence) { mEscapeState = ESC_NONE; } break; } } private void setAltCharSet(boolean alternateCharSet) { mAlternateCharSet = alternateCharSet; } private int nextTabStop(int cursorCol) { for (int i = cursorCol; i < mColumns; i++) { if (mTabStop[i]) { return i; } } return mColumns - 1; } private void doEscLSBQuest(byte b) { int mask = getDecFlagsMask(getArg0(0)); switch (b) { case 'h': // Esc [ ? Pn h - DECSET mDecFlags |= mask; break; case 'l': // Esc [ ? Pn l - DECRST mDecFlags &= ~mask; break; case 'r': // Esc [ ? Pn r - restore mDecFlags = (mDecFlags & ~mask) | (mSavedDecFlags & mask); break; case 's': // Esc [ ? Pn s - save mSavedDecFlags = (mSavedDecFlags & ~mask) | (mDecFlags & mask); break; default: parseArg(b); break; } // 132 column mode if ((mask & K_132_COLUMN_MODE_MASK) != 0) { // We don't actually set 132 cols, but we do want the // side effect of clearing the screen and homing the cursor. blockClear(0, 0, mColumns, mRows); setCursorRowCol(0, 0); } // origin mode if ((mask & K_ORIGIN_MODE_MASK) != 0) { // Home the cursor. setCursorPosition(0, 0); } } private int getDecFlagsMask(int argument) { if (argument >= 1 && argument <= 9) { return (1 << argument); } return 0; } private void startEscapeSequence(int escapeState) { mEscapeState = escapeState; mArgIndex = 0; for (int j = 0; j < MAX_ESCAPE_PARAMETERS; j++) { mArgs[j] = -1; } } private void doLinefeed() { int newCursorRow = mCursorRow + 1; if (newCursorRow >= mBottomMargin) { scroll(); newCursorRow = mBottomMargin - 1; } setCursorRow(newCursorRow); } private void continueSequence() { mContinueSequence = true; } private void continueSequence(int state) { mEscapeState = state; mContinueSequence = true; } private void doEscSelectLeftParen(byte b) { doSelectCharSet(true, b); } private void doEscSelectRightParen(byte b) { doSelectCharSet(false, b); } private void doSelectCharSet(boolean isG0CharSet, byte b) { switch (b) { case 'A': // United Kingdom character set break; case 'B': // ASCII set break; case '0': // Special Graphics break; case '1': // Alternate character set break; case '2': break; default: unknownSequence(b); } } private void doEscPound(byte b) { switch (b) { case '8': // Esc # 8 - DECALN alignment test mScreen.blockSet(0, 0, mColumns, mRows, 'E', getForeColor(), getBackColor()); break; default: unknownSequence(b); break; } } private void doEsc(byte b) { switch (b) { case '#': continueSequence(ESC_POUND); break; case '(': continueSequence(ESC_SELECT_LEFT_PAREN); break; case ')': continueSequence(ESC_SELECT_RIGHT_PAREN); break; case '7': // DECSC save cursor mSavedCursorRow = mCursorRow; mSavedCursorCol = mCursorCol; break; case '8': // DECRC restore cursor setCursorRowCol(mSavedCursorRow, mSavedCursorCol); break; case 'D': // INDEX doLinefeed(); break; case 'E': // NEL setCursorCol(0); doLinefeed(); break; case 'F': // Cursor to lower-left corner of screen setCursorRowCol(0, mBottomMargin - 1); break; case 'H': // Tab set mTabStop[mCursorCol] = true; break; case 'M': // Reverse index if (mCursorRow == 0) { mScreen.blockCopy(0, mTopMargin + 1, mColumns, mBottomMargin - (mTopMargin + 1), 0, mTopMargin); blockClear(0, mBottomMargin - 1, mColumns); } else { mCursorRow--; } break; case 'N': // SS2 unimplementedSequence(b); break; case '0': // SS3 unimplementedSequence(b); break; case 'P': // Device control string unimplementedSequence(b); break; case 'Z': // return terminal ID sendDeviceAttributes(); break; case '[': continueSequence(ESC_LEFT_SQUARE_BRACKET); break; case '=': // DECKPAM mbKeypadApplicationMode = true; break; case '>' : // DECKPNM mbKeypadApplicationMode = false; break; default: unknownSequence(b); break; } } private void doEscLeftSquareBracket(byte b) { switch (b) { case '@': // ESC [ Pn @ - ICH Insert Characters { int charsAfterCursor = mColumns - mCursorCol; int charsToInsert = Math.min(getArg0(1), charsAfterCursor); int charsToMove = charsAfterCursor - charsToInsert; mScreen.blockCopy(mCursorCol, mCursorRow, charsToMove, 1, mCursorCol + charsToInsert, mCursorRow); blockClear(mCursorCol, mCursorRow, charsToInsert); } break; case 'A': // ESC [ Pn A - Cursor Up setCursorRow(Math.max(mTopMargin, mCursorRow - getArg0(1))); break; case 'B': // ESC [ Pn B - Cursor Down setCursorRow(Math.min(mBottomMargin - 1, mCursorRow + getArg0(1))); break; case 'C': // ESC [ Pn C - Cursor Right setCursorCol(Math.min(mColumns - 1, mCursorCol + getArg0(1))); break; case 'D': // ESC [ Pn D - Cursor Left setCursorCol(Math.max(0, mCursorCol - getArg0(1))); break; case 'G': // ESC [ Pn G - Cursor Horizontal Absolute setCursorCol(Math.min(Math.max(1, getArg0(1)), mColumns) - 1); break; case 'H': // ESC [ Pn ; H - Cursor Position setHorizontalVerticalPosition(); break; case 'J': // ESC [ Pn J - Erase in Display switch (getArg0(0)) { case 0: // Clear below blockClear(mCursorCol, mCursorRow, mColumns - mCursorCol); blockClear(0, mCursorRow + 1, mColumns, mBottomMargin - (mCursorRow + 1)); break; case 1: // Erase from the start of the screen to the cursor. blockClear(0, mTopMargin, mColumns, mCursorRow - mTopMargin); blockClear(0, mCursorRow, mCursorCol + 1); break; case 2: // Clear all blockClear(0, mTopMargin, mColumns, mBottomMargin - mTopMargin); break; default: unknownSequence(b); break; } break; case 'K': // ESC [ Pn K - Erase in Line switch (getArg0(0)) { case 0: // Clear to right blockClear(mCursorCol, mCursorRow, mColumns - mCursorCol); break; case 1: // Erase start of line to cursor (including cursor) blockClear(0, mCursorRow, mCursorCol + 1); break; case 2: // Clear whole line blockClear(0, mCursorRow, mColumns); break; default: unknownSequence(b); break; } break; case 'L': // Insert Lines { int linesAfterCursor = mBottomMargin - mCursorRow; int linesToInsert = Math.min(getArg0(1), linesAfterCursor); int linesToMove = linesAfterCursor - linesToInsert; mScreen.blockCopy(0, mCursorRow, mColumns, linesToMove, 0, mCursorRow + linesToInsert); blockClear(0, mCursorRow, mColumns, linesToInsert); } break; case 'M': // Delete Lines { int linesAfterCursor = mBottomMargin - mCursorRow; int linesToDelete = Math.min(getArg0(1), linesAfterCursor); int linesToMove = linesAfterCursor - linesToDelete; mScreen.blockCopy(0, mCursorRow + linesToDelete, mColumns, linesToMove, 0, mCursorRow); blockClear(0, mCursorRow + linesToMove, mColumns, linesToDelete); } break; case 'P': // Delete Characters { int charsAfterCursor = mColumns - mCursorCol; int charsToDelete = Math.min(getArg0(1), charsAfterCursor); int charsToMove = charsAfterCursor - charsToDelete; mScreen.blockCopy(mCursorCol + charsToDelete, mCursorRow, charsToMove, 1, mCursorCol, mCursorRow); blockClear(mCursorCol + charsToMove, mCursorRow, charsToDelete); } break; case 'T': // Mouse tracking unimplementedSequence(b); break; case '?': // Esc [ ? -- start of a private mode set continueSequence(ESC_LEFT_SQUARE_BRACKET_QUESTION_MARK); break; case 'c': // Send device attributes sendDeviceAttributes(); break; case 'd': // ESC [ Pn d - Vert Position Absolute setCursorRow(Math.min(Math.max(1, getArg0(1)), mRows) - 1); break; case 'f': // Horizontal and Vertical Position setHorizontalVerticalPosition(); break; case 'g': // Clear tab stop switch (getArg0(0)) { case 0: mTabStop[mCursorCol] = false; break; case 3: for (int i = 0; i < mColumns; i++) { mTabStop[i] = false; } break; default: // Specified to have no effect. break; } break; case 'h': // Set Mode doSetMode(true); break; case 'l': // Reset Mode doSetMode(false); break; case 'm': // Esc [ Pn m - character attributes. selectGraphicRendition(); break; case 'r': // Esc [ Pn ; Pn r - set top and bottom margins { // The top margin defaults to 1, the bottom margin // (unusually for arguments) defaults to mRows. // // The escape sequence numbers top 1..23, but we // number top 0..22. // The escape sequence numbers bottom 2..24, and // so do we (because we use a zero based numbering // scheme, but we store the first line below the // bottom-most scrolling line. // As a result, we adjust the top line by -1, but // we leave the bottom line alone. // // Also require that top + 2 <= bottom int top = Math.max(0, Math.min(getArg0(1) - 1, mRows - 2)); int bottom = Math.max(top + 2, Math.min(getArg1(mRows), mRows)); mTopMargin = top; mBottomMargin = bottom; // The cursor is placed in the home position setCursorRowCol(mTopMargin, 0); } break; default: parseArg(b); break; } } private void selectGraphicRendition() { for (int i = 0; i <= mArgIndex; i++) { int code = mArgs[i]; if ( code < 0) { if (mArgIndex > 0) { continue; } else { code = 0; } } if (code == 0) { // reset mInverseColors = false; mForeColor = 7; mBackColor = 0; } else if (code == 1) { // bold mForeColor |= 0x8; } else if (code == 4) { // underscore mBackColor |= 0x8; } else if (code == 7) { // inverse mInverseColors = true; } else if (code >= 30 && code <= 37) { // foreground color mForeColor = (mForeColor & 0x8) | (code - 30); } else if (code >= 40 && code <= 47) { // background color mBackColor = (mBackColor & 0x8) | (code - 40); } else { if (Term.LOG_UNKNOWN_ESCAPE_SEQUENCES) { Log.w(Term.LOG_TAG, String.format("SGR unknown code %d", code)); } } } } private void blockClear(int sx, int sy, int w) { blockClear(sx, sy, w, 1); } private void blockClear(int sx, int sy, int w, int h) { mScreen.blockSet(sx, sy, w, h, ' ', getForeColor(), getBackColor()); } private int getForeColor() { return mInverseColors ? ((mBackColor & 0x7) | (mForeColor & 0x8)) : mForeColor; } private int getBackColor() { return mInverseColors ? ((mForeColor & 0x7) | (mBackColor & 0x8)) : mBackColor; } private void doSetMode(boolean newValue) { int modeBit = getArg0(0); switch (modeBit) { case 4: mInsertMode = newValue; break; case 20: mAutomaticNewlineMode = newValue; break; default: unknownParameter(modeBit); break; } } private void setHorizontalVerticalPosition() { // Parameters are Row ; Column setCursorPosition(getArg1(1) - 1, getArg0(1) - 1); } private void setCursorPosition(int x, int y) { int effectiveTopMargin = 0; int effectiveBottomMargin = mRows; if ((mDecFlags & K_ORIGIN_MODE_MASK) != 0) { effectiveTopMargin = mTopMargin; effectiveBottomMargin = mBottomMargin; } int newRow = Math.max(effectiveTopMargin, Math.min(effectiveTopMargin + y, effectiveBottomMargin - 1)); int newCol = Math.max(0, Math.min(x, mColumns - 1)); setCursorRowCol(newRow, newCol); } private void sendDeviceAttributes() { // This identifies us as a DEC vt100 with advanced // video options. This is what the xterm terminal // emulator sends. byte[] attributes = { /* VT100 */ (byte) 27, (byte) '[', (byte) '?', (byte) '1', (byte) ';', (byte) '2', (byte) 'c' /* VT220 (byte) 27, (byte) '[', (byte) '?', (byte) '6', (byte) '0', (byte) ';', (byte) '1', (byte) ';', (byte) '2', (byte) ';', (byte) '6', (byte) ';', (byte) '8', (byte) ';', (byte) '9', (byte) ';', (byte) '1', (byte) '5', (byte) ';', (byte) 'c' */ }; write(attributes); } /** * Send data to the shell process * @param data */ private void write(byte[] data) { try { mTermOut.write(data); mTermOut.flush(); } catch (IOException e) { // Ignore exception // We don't really care if the receiver isn't listening. // We just make a best effort to answer the query. } } private void scroll() { mScreen.scroll(mTopMargin, mBottomMargin, getForeColor(), getBackColor()); } /** * Process the next ASCII character of a parameter. * * @param b The next ASCII character of the paramater sequence. */ private void parseArg(byte b) { if (b >= '0' && b <= '9') { if (mArgIndex < mArgs.length) { int oldValue = mArgs[mArgIndex]; int thisDigit = b - '0'; int value; if (oldValue >= 0) { value = oldValue * 10 + thisDigit; } else { value = thisDigit; } mArgs[mArgIndex] = value; } continueSequence(); } else if (b == ';') { if (mArgIndex < mArgs.length) { mArgIndex++; } continueSequence(); } else { unknownSequence(b); } } private int getArg0(int defaultValue) { return getArg(0, defaultValue); } private int getArg1(int defaultValue) { return getArg(1, defaultValue); } private int getArg(int index, int defaultValue) { int result = mArgs[index]; if (result < 0) { result = defaultValue; } return result; } private void unimplementedSequence(byte b) { if (Term.LOG_UNKNOWN_ESCAPE_SEQUENCES) { logError("unimplemented", b); } finishSequence(); } private void unknownSequence(byte b) { if (Term.LOG_UNKNOWN_ESCAPE_SEQUENCES) { logError("unknown", b); } finishSequence(); } private void unknownParameter(int parameter) { if (Term.LOG_UNKNOWN_ESCAPE_SEQUENCES) { StringBuilder buf = new StringBuilder(); buf.append("Unknown parameter"); buf.append(parameter); logError(buf.toString()); } } private void logError(String errorType, byte b) { if (Term.LOG_UNKNOWN_ESCAPE_SEQUENCES) { StringBuilder buf = new StringBuilder(); buf.append(errorType); buf.append(" sequence "); buf.append(" EscapeState: "); buf.append(mEscapeState); buf.append(" char: '"); buf.append((char) b); buf.append("' ("); buf.append((int) b); buf.append(")"); boolean firstArg = true; for (int i = 0; i <= mArgIndex; i++) { int value = mArgs[i]; if (value >= 0) { if (firstArg) { firstArg = false; buf.append("args = "); } buf.append(String.format("%d; ", value)); } } logError(buf.toString()); } } private void logError(String error) { if (Term.LOG_UNKNOWN_ESCAPE_SEQUENCES) { Log.e(Term.LOG_TAG, error); } finishSequence(); } private void finishSequence() { mEscapeState = ESC_NONE; } private boolean autoWrapEnabled() { // Always enable auto wrap, because it's useful on a small screen return true; // return (mDecFlags & K_WRAPAROUND_MODE_MASK) != 0; } /** * Send an ASCII character to the screen. * * @param b the ASCII character to display. */ private void emit(byte b) { boolean autoWrap = autoWrapEnabled(); if (autoWrap) { if (mCursorCol == mColumns - 1 && mAboutToAutoWrap) { mScreen.setLineWrap(mCursorRow); mCursorCol = 0; if (mCursorRow + 1 < mBottomMargin) { mCursorRow++; } else { scroll(); } } } if (mInsertMode) { // Move character to right one space int destCol = mCursorCol + 1; if (destCol < mColumns) { mScreen.blockCopy(mCursorCol, mCursorRow, mColumns - destCol, 1, destCol, mCursorRow); } } mScreen.set(mCursorCol, mCursorRow, b, getForeColor(), getBackColor()); if (autoWrap) { mAboutToAutoWrap = (mCursorCol == mColumns - 1); } mCursorCol = Math.min(mCursorCol + 1, mColumns - 1); } private void setCursorRow(int row) { mCursorRow = row; mAboutToAutoWrap = false; } private void setCursorCol(int col) { mCursorCol = col; mAboutToAutoWrap = false; } private void setCursorRowCol(int row, int col) { mCursorRow = Math.min(row, mRows-1); mCursorCol = Math.min(col, mColumns-1); mAboutToAutoWrap = false; } /** * Reset the terminal emulator to its initial state. */ public void reset() { mCursorRow = 0; mCursorCol = 0; mArgIndex = 0; mContinueSequence = false; mEscapeState = ESC_NONE; mSavedCursorRow = 0; mSavedCursorCol = 0; mDecFlags = 0; mSavedDecFlags = 0; mInsertMode = false; mAutomaticNewlineMode = false; mTopMargin = 0; mBottomMargin = mRows; mAboutToAutoWrap = false; mForeColor = 7; mBackColor = 0; mInverseColors = false; mbKeypadApplicationMode = false; mAlternateCharSet = false; // mProcessedCharCount is preserved unchanged. setDefaultTabStops(); blockClear(0, 0, mColumns, mRows); } public String getTranscriptText() { return mScreen.getTranscriptText(); } } /** * Text renderer interface */ interface TextRenderer { int getCharacterWidth(); int getCharacterHeight(); void drawTextRun(Canvas canvas, float x, float y, int lineOffset, char[] text, int index, int count, boolean cursor, int foreColor, int backColor); } abstract class BaseTextRenderer implements TextRenderer { protected int[] mForePaint = { 0xff000000, // Black 0xffff0000, // Red 0xff00ff00, // green 0xffffff00, // yellow 0xff0000ff, // blue 0xffff00ff, // magenta 0xff00ffff, // cyan 0xffffffff // white -- is overridden by constructor }; protected int[] mBackPaint = { 0xff000000, // Black -- is overridden by constructor 0xffcc0000, // Red 0xff00cc00, // green 0xffcccc00, // yellow 0xff0000cc, // blue 0xffff00cc, // magenta 0xff00cccc, // cyan 0xffffffff // white }; protected final static int mCursorPaint = 0xff808080; public BaseTextRenderer(int forePaintColor, int backPaintColor) { mForePaint[7] = forePaintColor; mBackPaint[0] = backPaintColor; } } class Bitmap4x8FontRenderer extends BaseTextRenderer { private final static int kCharacterWidth = 4; private final static int kCharacterHeight = 8; private Bitmap mFont; private int mCurrentForeColor; private int mCurrentBackColor; private float[] mColorMatrix; private Paint mPaint; private static final float BYTE_SCALE = 1.0f / 255.0f; public Bitmap4x8FontRenderer(Resources resources, int forePaintColor, int backPaintColor) { super(forePaintColor, backPaintColor); mFont = BitmapFactory.decodeResource(resources, R.drawable.atari_small); mPaint = new Paint(); mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); } public int getCharacterWidth() { return kCharacterWidth; } public int getCharacterHeight() { return kCharacterHeight; } public void drawTextRun(Canvas canvas, float x, float y, int lineOffset, char[] text, int index, int count, boolean cursor, int foreColor, int backColor) { setColorMatrix(mForePaint[foreColor & 7], cursor ? mCursorPaint : mBackPaint[backColor & 7]); int destX = (int) x + kCharacterWidth * lineOffset; int destY = (int) y; Rect srcRect = new Rect(); Rect destRect = new Rect(); destRect.top = (destY - kCharacterHeight); destRect.bottom = destY; for(int i = 0; i < count; i++) { char c = text[i + index]; if ((cursor || (c != 32)) && (c < 128)) { int cellX = c & 31; int cellY = (c >> 5) & 3; int srcX = cellX * kCharacterWidth; int srcY = cellY * kCharacterHeight; srcRect.set(srcX, srcY, srcX + kCharacterWidth, srcY + kCharacterHeight); destRect.left = destX; destRect.right = destX + kCharacterWidth; canvas.drawBitmap(mFont, srcRect, destRect, mPaint); } destX += kCharacterWidth; } } private void setColorMatrix(int foreColor, int backColor) { if ((foreColor != mCurrentForeColor) || (backColor != mCurrentBackColor) || (mColorMatrix == null)) { mCurrentForeColor = foreColor; mCurrentBackColor = backColor; if (mColorMatrix == null) { mColorMatrix = new float[20]; mColorMatrix[18] = 1.0f; // Just copy Alpha } for (int component = 0; component < 3; component++) { int rightShift = (2 - component) << 3; int fore = 0xff & (foreColor >> rightShift); int back = 0xff & (backColor >> rightShift); int delta = back - fore; mColorMatrix[component * 6] = delta * BYTE_SCALE; mColorMatrix[component * 5 + 4] = fore; } mPaint.setColorFilter(new ColorMatrixColorFilter(mColorMatrix)); } } } class PaintRenderer extends BaseTextRenderer { public PaintRenderer(int fontSize, int forePaintColor, int backPaintColor) { super(forePaintColor, backPaintColor); mTextPaint = new Paint(); mTextPaint.setTypeface(Typeface.MONOSPACE); mTextPaint.setAntiAlias(true); mTextPaint.setTextSize(fontSize); mCharHeight = (int) Math.ceil(mTextPaint.getFontSpacing()); mCharAscent = (int) Math.ceil(mTextPaint.ascent()); mCharDescent = mCharHeight + mCharAscent; mCharWidth = (int) mTextPaint.measureText(EXAMPLE_CHAR, 0, 1); } public void drawTextRun(Canvas canvas, float x, float y, int lineOffset, char[] text, int index, int count, boolean cursor, int foreColor, int backColor) { if (cursor) { mTextPaint.setColor(mCursorPaint); } else { mTextPaint.setColor(mBackPaint[backColor & 0x7]); } float left = x + lineOffset * mCharWidth; canvas.drawRect(left, y + mCharAscent, left + count * mCharWidth, y + mCharDescent, mTextPaint); boolean bold = ( foreColor & 0x8 ) != 0; boolean underline = (backColor & 0x8) != 0; if (bold) { mTextPaint.setFakeBoldText(true); } if (underline) { mTextPaint.setUnderlineText(true); } mTextPaint.setColor(mForePaint[foreColor & 0x7]); canvas.drawText(text, index, count, left, y, mTextPaint); if (bold) { mTextPaint.setFakeBoldText(false); } if (underline) { mTextPaint.setUnderlineText(false); } } public int getCharacterHeight() { return mCharHeight; } public int getCharacterWidth() { return mCharWidth; } private Paint mTextPaint; private int mCharWidth; private int mCharHeight; private int mCharAscent; private int mCharDescent; private static final char[] EXAMPLE_CHAR = {'X'}; } /** * A multi-thread-safe produce-consumer byte array. * Only allows one producer and one consumer. */ class ByteQueue { public ByteQueue(int size) { mBuffer = new byte[size]; } public int getBytesAvailable() { synchronized(this) { return mStoredBytes; } } public int read(byte[] buffer, int offset, int length) throws InterruptedException { if (length + offset > buffer.length) { throw new IllegalArgumentException("length + offset > buffer.length"); } if (length < 0) { throw new IllegalArgumentException("length < 0"); } if (length == 0) { return 0; } synchronized(this) { while (mStoredBytes == 0) { wait(); } int totalRead = 0; int bufferLength = mBuffer.length; boolean wasFull = bufferLength == mStoredBytes; while (length > 0 && mStoredBytes > 0) { int oneRun = Math.min(bufferLength - mHead, mStoredBytes); int bytesToCopy = Math.min(length, oneRun); System.arraycopy(mBuffer, mHead, buffer, offset, bytesToCopy); mHead += bytesToCopy; if (mHead >= bufferLength) { mHead = 0; } mStoredBytes -= bytesToCopy; length -= bytesToCopy; offset += bytesToCopy; totalRead += bytesToCopy; } if (wasFull) { notify(); } return totalRead; } } public void write(byte[] buffer, int offset, int length) throws InterruptedException { if (length + offset > buffer.length) { throw new IllegalArgumentException("length + offset > buffer.length"); } if (length < 0) { throw new IllegalArgumentException("length < 0"); } if (length == 0) { return; } synchronized(this) { int bufferLength = mBuffer.length; boolean wasEmpty = mStoredBytes == 0; while (length > 0) { while(bufferLength == mStoredBytes) { wait(); } int tail = mHead + mStoredBytes; int oneRun; if (tail >= bufferLength) { tail = tail - bufferLength; oneRun = mHead - tail; } else { oneRun = bufferLength - tail; } int bytesToCopy = Math.min(oneRun, length); System.arraycopy(buffer, offset, mBuffer, tail, bytesToCopy); offset += bytesToCopy; mStoredBytes += bytesToCopy; length -= bytesToCopy; } if (wasEmpty) { notify(); } } } private byte[] mBuffer; private int mHead; private int mStoredBytes; } /** * A view on a transcript and a terminal emulator. Displays the text of the * transcript and the current cursor position of the terminal emulator. */ class EmulatorView extends View implements GestureDetector.OnGestureListener { /** * We defer some initialization until we have been layed out in the view * hierarchy. The boolean tracks when we know what our size is. */ private boolean mKnownSize; /** * Our transcript. Contains the screen and the transcript. */ private TranscriptScreen mTranscriptScreen; /** * Number of rows in the transcript. */ private static final int TRANSCRIPT_ROWS = 10000; /** * Total width of each character, in pixels */ private int mCharacterWidth; /** * Total height of each character, in pixels */ private int mCharacterHeight; /** * Used to render text */ private TextRenderer mTextRenderer; /** * Text size. Zero means 4 x 8 font. */ private int mTextSize; /** * Foreground color. */ private int mForeground; /** * Background color. */ private int mBackground; /** * Used to paint the cursor */ private Paint mCursorPaint; private Paint mBackgroundPaint; /** * Our terminal emulator. We use this to get the current cursor position. */ private TerminalEmulator mEmulator; /** * The number of rows of text to display. */ private int mRows; /** * The number of columns of text to display. */ private int mColumns; /** * The number of columns that are visible on the display. */ private int mVisibleColumns; /** * The top row of text to display. Ranges from -activeTranscriptRows to 0 */ private int mTopRow; private int mLeftColumn; private FileDescriptor mTermFd; /** * Used to receive data from the remote process. */ private FileInputStream mTermIn; private FileOutputStream mTermOut; private ByteQueue mByteQueue; /** * Used to temporarily hold data received from the remote process. Allocated * once and used permanently to minimize heap thrashing. */ private byte[] mReceiveBuffer; /** * Our private message id, which we use to receive new input from the * remote process. */ private static final int UPDATE = 1; /** * Thread that polls for input from the remote process */ private Thread mPollingThread; private GestureDetector mGestureDetector; private float mScrollRemainder; /** * Our message handler class. Implements a periodic callback. */ private final Handler mHandler = new Handler() { /** * Handle the callback message. Call our enclosing class's update * method. * * @param msg The callback message. */ public void handleMessage(Message msg) { if (msg.what == UPDATE) { update(); } } }; public EmulatorView(Context context) { super(context); commonConstructor(); } public void setColors(int foreground, int background) { mForeground = foreground; mBackground = background; updateText(); } public String getTranscriptText() { return mEmulator.getTranscriptText(); } public void resetTerminal() { mEmulator.reset(); invalidate(); } @Override public boolean onCheckIsTextEditor() { return true; } @Override public InputConnection onCreateInputConnection(EditorInfo outAttrs) { return new BaseInputConnection(this, false) { public boolean beginBatchEdit() { return true; } public boolean clearMetaKeyStates(int states) { return true; } public boolean commitCompletion(CompletionInfo text) { return true; } public boolean commitText(CharSequence text, int newCursorPosition) { sendText(text); return true; } public boolean deleteSurroundingText(int leftLength, int rightLength) { return true; } public boolean endBatchEdit() { return true; } public boolean finishComposingText() { return true; } public int getCursorCapsMode(int reqModes) { return 0; } public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) { return null; } public CharSequence getTextAfterCursor(int n, int flags) { return null; } public CharSequence getTextBeforeCursor(int n, int flags) { return null; } public boolean performEditorAction(int actionCode) { if(actionCode == EditorInfo.IME_ACTION_UNSPECIFIED) { // The "return" key has been pressed on the IME. sendText("\n"); return true; } return false; } public boolean performContextMenuAction(int id) { return true; } public boolean performPrivateCommand(String action, Bundle data) { return true; } public boolean sendKeyEvent(KeyEvent event) { if (event.getAction() == KeyEvent.ACTION_DOWN) { switch(event.getKeyCode()) { case KeyEvent.KEYCODE_DEL: sendChar(127); break; } } return true; } public boolean setComposingText(CharSequence text, int newCursorPosition) { return true; } public boolean setSelection(int start, int end) { return true; } private void sendChar(int c) { try { mTermOut.write(c); } catch (IOException ex) { } } private void sendText(CharSequence text) { int n = text.length(); try { for(int i = 0; i < n; i++) { char c = text.charAt(i); mTermOut.write(c); } } catch (IOException e) { } } }; } public boolean getKeypadApplicationMode() { return mEmulator.getKeypadApplicationMode(); } public EmulatorView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public EmulatorView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); TypedArray a = context.obtainStyledAttributes(android.R.styleable.View); initializeScrollbars(a); a.recycle(); commonConstructor(); } private void commonConstructor() { mTextRenderer = null; mCursorPaint = new Paint(); mCursorPaint.setARGB(255,128,128,128); mBackgroundPaint = new Paint(); mTopRow = 0; mLeftColumn = 0; mGestureDetector = new GestureDetector(this); mGestureDetector.setIsLongpressEnabled(false); setVerticalScrollBarEnabled(true); } @Override protected int computeVerticalScrollRange() { return mTranscriptScreen.getActiveRows(); } @Override protected int computeVerticalScrollExtent() { return mRows; } @Override protected int computeVerticalScrollOffset() { return mTranscriptScreen.getActiveRows() + mTopRow - mRows; } /** * Call this to initialize the view. * * @param termFd the file descriptor * @param termOut the output stream for the pseudo-teletype */ public void initialize(FileDescriptor termFd, FileOutputStream termOut) { mTermOut = termOut; mTermFd = termFd; mTextSize = 10; mForeground = Term.WHITE; mBackground = Term.BLACK; updateText(); mTermIn = new FileInputStream(mTermFd); mReceiveBuffer = new byte[4 * 1024]; mByteQueue = new ByteQueue(4 * 1024); } /** * Accept a sequence of bytes (typically from the pseudo-tty) and process * them. * * @param buffer a byte array containing bytes to be processed * @param base the index of the first byte in the buffer to process * @param length the number of bytes to process */ public void append(byte[] buffer, int base, int length) { mEmulator.append(buffer, base, length); ensureCursorVisible(); invalidate(); } /** * Page the terminal view (scroll it up or down by delta screenfulls.) * * @param delta the number of screens to scroll. Positive means scroll down, * negative means scroll up. */ public void page(int delta) { mTopRow = Math.min(0, Math.max(-(mTranscriptScreen .getActiveTranscriptRows()), mTopRow + mRows * delta)); invalidate(); } /** * Page the terminal view horizontally. * * @param deltaColumns the number of columns to scroll. Positive scrolls to * the right. */ public void pageHorizontal(int deltaColumns) { mLeftColumn = Math.max(0, Math.min(mLeftColumn + deltaColumns, mColumns - mVisibleColumns)); invalidate(); } /** * Sets the text size, which in turn sets the number of rows and columns * * @param fontSize the new font size, in pixels. */ public void setTextSize(int fontSize) { mTextSize = fontSize; updateText(); } // Begin GestureDetector.OnGestureListener methods public boolean onSingleTapUp(MotionEvent e) { return true; } public void onLongPress(MotionEvent e) { } public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { distanceY += mScrollRemainder; int deltaRows = (int) (distanceY / mCharacterHeight); mScrollRemainder = distanceY - deltaRows * mCharacterHeight; mTopRow = Math.min(0, Math.max(-(mTranscriptScreen .getActiveTranscriptRows()), mTopRow + deltaRows)); invalidate(); return true; } public void onSingleTapConfirmed(MotionEvent e) { } public boolean onJumpTapDown(MotionEvent e1, MotionEvent e2) { // Scroll to bottom mTopRow = 0; invalidate(); return true; } public boolean onJumpTapUp(MotionEvent e1, MotionEvent e2) { // Scroll to top mTopRow = -mTranscriptScreen.getActiveTranscriptRows(); invalidate(); return true; } public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { // TODO: add animation man's (non animated) fling mScrollRemainder = 0.0f; onScroll(e1, e2, 2 * velocityX, -2 * velocityY); return true; } public void onShowPress(MotionEvent e) { } public boolean onDown(MotionEvent e) { mScrollRemainder = 0.0f; return true; } // End GestureDetector.OnGestureListener methods @Override public boolean onTouchEvent(MotionEvent ev) { return mGestureDetector.onTouchEvent(ev); } private void updateText() { if (mTextSize > 0) { mTextRenderer = new PaintRenderer(mTextSize, mForeground, mBackground); } else { mTextRenderer = new Bitmap4x8FontRenderer(getResources(), mForeground, mBackground); } mBackgroundPaint.setColor(mBackground); mCharacterWidth = mTextRenderer.getCharacterWidth(); mCharacterHeight = mTextRenderer.getCharacterHeight(); if (mKnownSize) { updateSize(getWidth(), getHeight()); } } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { updateSize(w, h); if (!mKnownSize) { mKnownSize = true; // Set up a thread to read input from the // pseudo-teletype: mPollingThread = new Thread(new Runnable() { public void run() { try { while(true) { int read = mTermIn.read(mBuffer); mByteQueue.write(mBuffer, 0, read); mHandler.sendMessage( mHandler.obtainMessage(UPDATE)); } } catch (IOException e) { } catch (InterruptedException e) { } } private byte[] mBuffer = new byte[4096]; }); mPollingThread.setName("Input reader"); mPollingThread.start(); } } private void updateSize(int w, int h) { mColumns = w / mCharacterWidth; mRows = h / mCharacterHeight; // Inform the attached pty of our new size: Exec.setPtyWindowSize(mTermFd, mRows, mColumns, w, h); if (mTranscriptScreen != null) { mEmulator.updateSize(mColumns, mRows); } else { mTranscriptScreen = new TranscriptScreen(mColumns, TRANSCRIPT_ROWS, mRows, 0, 7); mEmulator = new TerminalEmulator(mTranscriptScreen, mColumns, mRows, mTermOut); } // Reset our paging: mTopRow = 0; mLeftColumn = 0; invalidate(); } void updateSize() { if (mKnownSize) { updateSize(getWidth(), getHeight()); } } /** * Look for new input from the ptty, send it to the terminal emulator. */ private void update() { int bytesAvailable = mByteQueue.getBytesAvailable(); int bytesToRead = Math.min(bytesAvailable, mReceiveBuffer.length); try { int bytesRead = mByteQueue.read(mReceiveBuffer, 0, bytesToRead); append(mReceiveBuffer, 0, bytesRead); } catch (InterruptedException e) { } } @Override protected void onDraw(Canvas canvas) { int w = getWidth(); int h = getHeight(); canvas.drawRect(0, 0, w, h, mBackgroundPaint); mVisibleColumns = w / mCharacterWidth; float x = -mLeftColumn * mCharacterWidth; float y = mCharacterHeight; int endLine = mTopRow + mRows; int cx = mEmulator.getCursorCol(); int cy = mEmulator.getCursorRow(); for (int i = mTopRow; i < endLine; i++) { int cursorX = -1; if (i == cy) { cursorX = cx; } mTranscriptScreen.drawText(i, canvas, x, y, mTextRenderer, cursorX); y += mCharacterHeight; } } private void ensureCursorVisible() { mTopRow = 0; if (mVisibleColumns > 0) { int cx = mEmulator.getCursorCol(); int visibleCursorX = mEmulator.getCursorCol() - mLeftColumn; if (visibleCursorX < 0) { mLeftColumn = cx; } else if (visibleCursorX >= mVisibleColumns) { mLeftColumn = (cx - mVisibleColumns) + 1; } } } } /** * An ASCII key listener. Supports control characters and escape. Keeps track of * the current state of the alt, shift, and control keys. */ class TermKeyListener { /** * The state engine for a modifier key. Can be pressed, released, locked, * and so on. * */ private class ModifierKey { private int mState; private static final int UNPRESSED = 0; private static final int PRESSED = 1; private static final int RELEASED = 2; private static final int USED = 3; private static final int LOCKED = 4; /** * Construct a modifier key. UNPRESSED by default. * */ public ModifierKey() { mState = UNPRESSED; } public void onPress() { switch (mState) { case PRESSED: // This is a repeat before use break; case RELEASED: mState = LOCKED; break; case USED: // This is a repeat after use break; case LOCKED: mState = UNPRESSED; break; default: mState = PRESSED; break; } } public void onRelease() { switch (mState) { case USED: mState = UNPRESSED; break; case PRESSED: mState = RELEASED; break; default: // Leave state alone break; } } public void adjustAfterKeypress() { switch (mState) { case PRESSED: mState = USED; break; case RELEASED: mState = UNPRESSED; break; default: // Leave state alone break; } } public boolean isActive() { return mState != UNPRESSED; } } private ModifierKey mAltKey = new ModifierKey(); private ModifierKey mCapKey = new ModifierKey(); private ModifierKey mControlKey = new ModifierKey(); /** * Construct a term key listener. * */ public TermKeyListener() { } public void handleControlKey(boolean down) { if (down) { mControlKey.onPress(); } else { mControlKey.onRelease(); } } /** * Handle a keyDown event. * * @param keyCode the keycode of the keyDown event * @return the ASCII byte to transmit to the pseudo-teletype, or -1 if this * event does not produce an ASCII byte. */ public int keyDown(int keyCode, KeyEvent event) { int result = -1; switch (keyCode) { case KeyEvent.KEYCODE_ALT_RIGHT: case KeyEvent.KEYCODE_ALT_LEFT: mAltKey.onPress(); break; case KeyEvent.KEYCODE_SHIFT_LEFT: case KeyEvent.KEYCODE_SHIFT_RIGHT: mCapKey.onPress(); break; case KeyEvent.KEYCODE_ENTER: // Convert newlines into returns. The vt100 sends a // '\r' when the 'Return' key is pressed, but our // KeyEvent translates this as a '\n'. result = '\r'; break; case KeyEvent.KEYCODE_DEL: // Convert DEL into 127 (instead of 8) result = 127; break; default: { result = event.getUnicodeChar( (mCapKey.isActive() ? KeyEvent.META_SHIFT_ON : 0) | (mAltKey.isActive() ? KeyEvent.META_ALT_ON : 0)); break; } } if (mControlKey.isActive()) { // Search is the control key. if (result >= 'a' && result <= 'z') { result = (char) (result - 'a' + '\001'); } else if (result == ' ') { result = 0; } else if ((result == '[') || (result == '1')) { result = 27; } else if ((result == '\\') || (result == '.')) { result = 28; } else if ((result == ']') || (result == '0')) { result = 29; } else if ((result == '^') || (result == '6')) { result = 30; // control-^ } else if ((result == '_') || (result == '5')) { result = 31; } } if (result > -1) { mAltKey.adjustAfterKeypress(); mCapKey.adjustAfterKeypress(); mControlKey.adjustAfterKeypress(); } return result; } /** * Handle a keyUp event. * * @param keyCode the keyCode of the keyUp event */ public void keyUp(int keyCode) { switch (keyCode) { case KeyEvent.KEYCODE_ALT_LEFT: case KeyEvent.KEYCODE_ALT_RIGHT: mAltKey.onRelease(); break; case KeyEvent.KEYCODE_SHIFT_LEFT: case KeyEvent.KEYCODE_SHIFT_RIGHT: mCapKey.onRelease(); break; default: // Ignore other keyUps break; } } }