Files
android_development/apps/Term/src/com/android/term/Term.java
Jack Palevich edb55982e1 Fix Terminal Preferences, Resolution
The terminal app was using the preferences framework incorrectly: it was
trying to keep its own authoritative copies of the preferences, instead
of relying in the preference framework to store the preferences. This
lead to some race conditions, and made terminal's preferences not work
correctly in Donut or Eclair.

Terminal now uses preferences in a more straightforward way.

As long as I was editing terminal, I made two additional changes:

Term now honors the display density. This makes the text more readable on
high-density devices.

An I put in some error checks for empty shell and first-command strings.

Fixes bug 2177356	Term program on Sholes: preferences doesn't work
2009-10-09 20:16:26 -07:00

3275 lines
97 KiB
Java

/*
* 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 java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
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.Handler;
import android.os.Message;
import android.preference.PreferenceManager;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
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;
/**
* 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.
*/
private FileDescriptor mTermFd;
/**
* 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);
readPrefs();
setContentView(R.layout.term_activity);
mEmulatorView = (EmulatorView) findViewById(EMULATOR_VIEW);
startListening();
mKeyListener = new TermKeyListener();
mEmulatorView.setFocusable(true);
mEmulatorView.setFocusableInTouchMode(true);
mEmulatorView.requestFocus();
mEmulatorView.register(mKeyListener);
updatePrefs();
}
@Override
public void onDestroy() {
super.onDestroy();
if (mTermFd != null) {
Exec.close(mTermFd);
mTermFd = null;
}
}
private void startListening() {
int[] processId = new int[1];
createSubprocess(processId);
final int procId = processId[0];
final Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
}
};
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.equals("")) {
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.equals("")) {
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() {
DisplayMetrics metrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(metrics);
mEmulatorView.setTextSize((int) (mFontSize * metrics.density));
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 onResume() {
super.onResume();
readPrefs();
updatePrefs();
}
@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(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(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;
private TermKeyListener mKeyListener;
/**
* 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.
*/
@Override
public void handleMessage(Message msg) {
if (msg.what == UPDATE) {
update();
}
}
};
public EmulatorView(Context context) {
super(context);
commonConstructor();
}
public void register(TermKeyListener listener) {
mKeyListener = listener;
}
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) {
@Override
public boolean beginBatchEdit() {
return true;
}
@Override
public boolean clearMetaKeyStates(int states) {
return true;
}
@Override
public boolean commitCompletion(CompletionInfo text) {
return true;
}
@Override
public boolean commitText(CharSequence text, int newCursorPosition) {
sendText(text);
return true;
}
@Override
public boolean deleteSurroundingText(int leftLength, int rightLength) {
return true;
}
@Override
public boolean endBatchEdit() {
return true;
}
@Override
public boolean finishComposingText() {
return true;
}
@Override
public int getCursorCapsMode(int reqModes) {
return 0;
}
@Override
public ExtractedText getExtractedText(ExtractedTextRequest request,
int flags) {
return null;
}
@Override
public CharSequence getTextAfterCursor(int n, int flags) {
return null;
}
@Override
public CharSequence getTextBeforeCursor(int n, int flags) {
return null;
}
@Override
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;
}
@Override
public boolean performContextMenuAction(int id) {
return true;
}
@Override
public boolean performPrivateCommand(String action, Bundle data) {
return true;
}
@Override
public boolean sendKeyEvent(KeyEvent event) {
if (event.getAction() == KeyEvent.ACTION_DOWN) {
switch(event.getKeyCode()) {
case KeyEvent.KEYCODE_DEL:
sendChar(127);
break;
}
}
return true;
}
@Override
public boolean setComposingText(CharSequence text, int newCursorPosition) {
return true;
}
@Override
public boolean setSelection(int start, int end) {
return true;
}
private void sendChar(int c) {
try {
mapAndSend(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);
mapAndSend(c);
}
} catch (IOException e) {
}
}
private void mapAndSend(int c) throws IOException {
mTermOut.write(
mKeyListener.mapControlChar(c));
}
};
}
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();
}
}
public int mapControlChar(int ch) {
int result = ch;
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 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;
}
}
result = mapControlChar(result);
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;
}
}
}