| /* |
| * 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; |
| } |
| } |
| } |