/* * Copyright (C) 2007 The Android Open Source Project * Copyright (C) 2011 John Pritchard, Syntelos * * 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 ob.droid.term; import android.content.Context; 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.os.Bundle; import android.os.Handler; import android.os.Message; import android.util.AttributeSet; import android.util.Log; import android.view.View; import android.view.GestureDetector; import android.view.KeyEvent; import android.view.MotionEvent; 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 ob.droid.Connection; import ob.droid.R; /** * 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(context); } 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(context); } private void commonConstructor(Context context) { mTextRenderer = null; mCursorPaint = new Paint(); mCursorPaint.setARGB(255,128,128,128); mBackgroundPaint = new Paint(); mTopRow = 0; mLeftColumn = 0; mGestureDetector = new GestureDetector(context, this, null); mGestureDetector.setIsLongpressEnabled(false); setVerticalScrollBarEnabled(true); } /** * Call this to initialize the view. * * @param termFd the file descriptor * @param termOut the output stream for the pseudo-teletype */ public void init(Connection connection) { this.connection = connection; mTextSize = 10; mForeground = Term.WHITE; mBackground = Term.BLACK; updateText(); mReceiveBuffer = new byte[4 * 1024]; mByteQueue = new ByteQueue(4 * 1024); } 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(); } @Override protected int computeVerticalScrollRange() { return mTranscriptScreen.getActiveRows(); } @Override protected int computeVerticalScrollExtent() { return mRows; } @Override protected int computeVerticalScrollOffset() { return mTranscriptScreen.getActiveRows() + mTopRow - mRows; } /** * 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); } // 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; } } } }