blob: 1f43843a3aeb8d28c639634ecf15f5c1f57c70d5 [file] [log] [blame]
/*
* 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;
}
}
}