Project: Android-Terminal-Emulator
/*
 * 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 jackpal.androidterm.emulatorview; 
 
import java.io.IOException; 
 
import android.content.Context; 
import android.graphics.Canvas; 
import android.graphics.Paint; 
import android.os.Build; 
import android.os.Bundle; 
import android.os.Handler; 
import android.text.ClipboardManager; 
import android.util.AttributeSet; 
import android.util.DisplayMetrics; 
import android.util.Log; 
import android.view.GestureDetector; 
import android.view.KeyEvent; 
import android.view.MotionEvent; 
import android.view.View; 
import android.view.inputmethod.BaseInputConnection; 
import android.view.inputmethod.CompletionInfo; 
import android.view.inputmethod.CorrectionInfo; 
import android.view.inputmethod.EditorInfo; 
import android.view.inputmethod.ExtractedText; 
import android.view.inputmethod.ExtractedTextRequest; 
import android.view.inputmethod.InputConnection; 
import android.widget.Scroller; 
 
/**
 * A view on a {@link TermSession}.  Displays the terminal emulator's screen, 
 * provides access to its scrollback buffer, and passes input through to the 
 * terminal emulator. 
 * <p> 
 * If this view is inflated from an XML layout, you need to call {@link 
 * #attachSession attachSession} and {@link #setDensity setDensity} before using 
 * the view.  If creating this view from code, use the {@link 
 * #EmulatorView(Context, TermSession, DisplayMetrics)} constructor, which will 
 * take care of this for you. 
 */
 
public class EmulatorView extends View implements GestureDetector.OnGestureListener { 
    private final String TAG = "EmulatorView"
    private final boolean LOG_KEY_EVENTS = false
    private final boolean LOG_IME = false
 
    /**
     * 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; 
 
    // Set if initialization was deferred because a TermSession wasn't attached 
    private boolean mDeferInit = false
 
    private int mVisibleWidth; 
    private int mVisibleHeight; 
 
    private TermSession mTermSession; 
 
    /**
     * Our transcript. Contains the screen and the transcript. 
     */
 
    private TranscriptScreen mTranscriptScreen; 
 
    /**
     * Total width of each character, in pixels 
     */
 
    private float mCharacterWidth; 
 
    /**
     * Total height of each character, in pixels 
     */
 
    private int mCharacterHeight; 
 
    /**
     * Top-of-screen margin 
     */
 
    private int mTopOfScreenMargin; 
 
    /**
     * Used to render text 
     */
 
    private TextRenderer mTextRenderer; 
 
    /**
     * Text size. Zero means 4 x 8 font. 
     */
 
    private int mTextSize = 10
 
    private int mCursorStyle; 
    private int mCursorBlink; 
 
    /**
     * Color scheme (default foreground/background colors). 
     */
 
    private ColorScheme mColorScheme = BaseTextRenderer.defaultColorScheme; 
 
    /**
     * Used to paint the cursor 
     */
 
    private Paint mCursorPaint; 
 
    private Paint mForegroundPaint; 
 
    private Paint mBackgroundPaint; 
 
    private boolean mUseCookedIme; 
 
    /**
     * 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 static final int CURSOR_BLINK_PERIOD = 1000
 
    private boolean mCursorVisible = true
 
    private boolean mIsSelectingText = false
 
    private boolean mBackKeySendsCharacter = false
    private int mControlKeyCode; 
    private int mFnKeyCode; 
    private boolean mIsControlKeySent = false
    private boolean mIsFnKeySent = false
 
    private float mDensity; 
 
    private float mScaledDensity; 
    private static final int SELECT_TEXT_OFFSET_Y = -40
    private int mSelXAnchor = -1
    private int mSelYAnchor = -1
    private int mSelX1 = -1
    private int mSelY1 = -1
    private int mSelX2 = -1
    private int mSelY2 = -1
 
    private boolean mIsActive = false
 
    /**
     * Routing alt and meta keyCodes away from the IME allows Alt key processing to work on 
     * the Asus Transformer TF101. 
     * It doesn't seem to harm anything else, but it also doesn't seem to be 
     * required on other platforms. 
     * 
     * This test should be refined as we learn more. 
     */
 
    private final static boolean sTrapAltAndMeta = Build.MODEL.contains("Transformer TF101"); 
 
    private Runnable mBlinkCursor = new Runnable() { 
        public void run() { 
            if (mCursorBlink != 0) { 
                mCursorVisible = ! mCursorVisible; 
                mHandler.postDelayed(this, CURSOR_BLINK_PERIOD); 
            } else { 
                mCursorVisible = true
            } 
            // Perhaps just invalidate the character with the cursor. 
            invalidate(); 
        } 
    }; 
 
    private GestureDetector mGestureDetector; 
    private GestureDetector.OnGestureListener mExtGestureListener; 
    private Scroller mScroller; 
    private Runnable mFlingRunner = new Runnable() { 
        public void run() { 
            if (mScroller.isFinished()) { 
                return
            } 
 
            boolean more = mScroller.computeScrollOffset(); 
            int newTopRow = mScroller.getCurrY(); 
            if (newTopRow != mTopRow) { 
                mTopRow = newTopRow; 
                invalidate(); 
            } 
 
            if (more) { 
                post(this); 
            } 
 
        } 
    }; 
    private float mScrollRemainder; 
    private TermKeyListener mKeyListener; 
 
    private String mImeBuffer = ""
 
    /**
     * Our message handler class. Implements a periodic callback. 
     */
 
    private final Handler mHandler = new Handler(); 
 
    /**
     * Called by the TermSession when the contents of the view need updating 
     */
 
    private UpdateCallback mUpdateNotify = new UpdateCallback() { 
        public void onUpdate() { 
            if ( mIsSelectingText ) { 
                int rowShift = mEmulator.getScrollCounter(); 
                mSelY1 -= rowShift; 
                mSelY2 -= rowShift; 
                mSelYAnchor -= rowShift; 
            } 
            mEmulator.clearScrollCounter(); 
            ensureCursorVisible(); 
            invalidate(); 
        } 
    }; 
 
    /**
     * Create an <code>EmulatorView</code> for a {@link TermSession}. 
     * 
     * @param context The {@link Context} for the view. 
     * @param session The {@link TermSession} this view will be displaying. 
     * @param metrics The {@link DisplayMetrics} of the screen on which the view 
     *                will be displayed. 
     */
 
    public EmulatorView(Context context, TermSession session, DisplayMetrics metrics) { 
        super(context); 
        attachSession(session); 
        setDensity(metrics); 
        commonConstructor(context); 
    } 
 
    /**
     * Constructor called when inflating this view from XML. 
     * <p> 
     * You should call {@link #attachSession attachSession} and {@link 
     * #setDensity setDensity} before using an <code>EmulatorView</code> created 
     * using this constructor. 
     */
 
    public EmulatorView(Context context, AttributeSet attrs) { 
        super(context, attrs); 
        commonConstructor(context); 
    } 
 
    /**
     * Constructor called when inflating this view from XML with a 
     * default style set. 
     * <p> 
     * You should call {@link #attachSession attachSession} and {@link 
     * #setDensity setDensity} before using an <code>EmulatorView</code> created 
     * using this constructor. 
     */
 
    public EmulatorView(Context context, AttributeSet attrs, int defStyle) { 
        super(context, attrs, defStyle); 
        commonConstructor(context); 
    } 
 
    private void commonConstructor(Context context) { 
        // TODO: See if we want to use the API level 11 constructor to get new flywheel feature. 
        mScroller = new Scroller(context); 
    } 
 
    /**
     * Attach a {@link TermSession} to this view. 
     * 
     * @param session The {@link TermSession} this view will be displaying. 
     */
 
    public void attachSession(TermSession session) { 
        mTextRenderer = null
        mCursorPaint = new Paint(); 
        mCursorPaint.setARGB(255,128,128,128); 
        mForegroundPaint = new Paint(); 
        mBackgroundPaint = new Paint(); 
        mTopRow = 0
        mLeftColumn = 0
        mGestureDetector = new GestureDetector(this); 
        // mGestureDetector.setIsLongpressEnabled(false); 
        setVerticalScrollBarEnabled(true); 
        setFocusable(true); 
        setFocusableInTouchMode(true); 
 
        mTermSession = session; 
 
        mKeyListener = new TermKeyListener(session); 
 
        // Do init now if it was deferred until a TermSession was attached 
        if (mDeferInit) { 
            mDeferInit = false
            mKnownSize = true
            initialize(); 
        } 
    } 
 
    /**
     * Update the screen density for the screen on which the view is displayed. 
     * 
     * @param metrics The {@link DisplayMetrics} of the screen. 
     */
 
    public void setDensity(DisplayMetrics metrics) { 
        if (mDensity == 0) { 
            // First time we've known the screen density, so update font size 
            mTextSize = (int) (mTextSize * metrics.density); 
        } 
        mDensity = metrics.density; 
        mScaledDensity = metrics.scaledDensity; 
    } 
 
    /**
     * Inform the view that it is now visible on screen. 
     */
 
    public void onResume() { 
        mIsActive = true
        updateSize(false); 
        if (mCursorBlink != 0) { 
            mHandler.postDelayed(mBlinkCursor, CURSOR_BLINK_PERIOD); 
        } 
    } 
 
    /**
     * Inform the view that it is no longer visible on the screen. 
     */
 
    public void onPause() { 
        if (mCursorBlink != 0) { 
            mHandler.removeCallbacks(mBlinkCursor); 
        } 
        mIsActive = false
    } 
 
    /**
     * Set this <code>EmulatorView</code>'s color scheme. 
     * 
     * @param scheme The {@link ColorScheme} to use (use null for the default 
     *               scheme). 
     * @see TermSession#setColorScheme 
     * @see ColorScheme 
     */
 
    public void setColorScheme(ColorScheme scheme) { 
        if (scheme == null) { 
            mColorScheme = BaseTextRenderer.defaultColorScheme; 
        } else { 
            mColorScheme = scheme; 
        } 
        updateText(); 
    } 
 
    @Override 
    public boolean onCheckIsTextEditor() { 
        return true
    } 
 
    @Override 
    public InputConnection onCreateInputConnection(EditorInfo outAttrs) { 
        outAttrs.inputType = mUseCookedIme ? 
                EditorInfo.TYPE_CLASS_TEXT : 
                EditorInfo.TYPE_NULL; 
        return new BaseInputConnection(thistrue) { 
            /**
             * Used to handle composing text requests 
             */
 
            private int mCursor; 
            private int mComposingTextStart; 
            private int mComposingTextEnd; 
            private int mSelectedTextStart; 
            private int mSelectedTextEnd; 
 
            private void sendText(CharSequence text) { 
                int n = text.length(); 
                char c; 
                try { 
                    for(int i = 0; i < n; i++) { 
                        c = text.charAt(i); 
                        if (Character.isHighSurrogate(c)) { 
                            int codePoint; 
                            if (++i < n) { 
                                codePoint = Character.toCodePoint(c, text.charAt(i)); 
                            } else { 
                                // Unicode Replacement Glyph, aka white question mark in black diamond. 
                                codePoint = '\ufffd'; 
                            } 
                            mapAndSend(codePoint); 
                        } else { 
                            mapAndSend(c); 
                        } 
                    } 
                } catch (IOException e) { 
                    Log.e(TAG, "error writing ", e); 
                } 
            } 
 
            private void mapAndSend(int c) throws IOException { 
                int result = mKeyListener.mapControlChar(c); 
                if (result < TermKeyListener.KEYCODE_OFFSET) { 
                    mTermSession.write(result); 
                } else { 
                    mKeyListener.handleKeyCode(result - TermKeyListener.KEYCODE_OFFSET, getKeypadApplicationMode()); 
                } 
                clearSpecialKeyStatus(); 
            } 
 
            public boolean beginBatchEdit() { 
                if (LOG_IME) { 
                    Log.w(TAG, "beginBatchEdit"); 
                } 
                setImeBuffer(""); 
                mCursor = 0
                mComposingTextStart = 0
                mComposingTextEnd = 0
                return true
            } 
 
            public boolean clearMetaKeyStates(int arg0) { 
                if (LOG_IME) { 
                    Log.w(TAG, "clearMetaKeyStates " + arg0); 
                } 
                return false
            } 
 
            public boolean commitCompletion(CompletionInfo arg0) { 
                if (LOG_IME) { 
                    Log.w(TAG, "commitCompletion " + arg0); 
                } 
                return false
            } 
 
            public boolean endBatchEdit() { 
                if (LOG_IME) { 
                    Log.w(TAG, "endBatchEdit"); 
                } 
                return true
            } 
 
            public boolean finishComposingText() { 
                if (LOG_IME) { 
                    Log.w(TAG, "finishComposingText"); 
                } 
                sendText(mImeBuffer); 
                setImeBuffer(""); 
                mComposingTextStart = 0
                mComposingTextEnd = 0
                mCursor = 0
                return true
            } 
 
            public int getCursorCapsMode(int arg0) { 
                if (LOG_IME) { 
                    Log.w(TAG, "getCursorCapsMode(" + arg0 + ")"); 
                } 
                return 0
            } 
 
            public ExtractedText getExtractedText(ExtractedTextRequest arg0, 
                    int arg1) { 
                if (LOG_IME) { 
                    Log.w(TAG, "getExtractedText" + arg0 + "," + arg1); 
                } 
                return null
            } 
 
            public CharSequence getTextAfterCursor(int n, int flags) { 
                if (LOG_IME) { 
                    Log.w(TAG, "getTextAfterCursor(" + n + "," + flags + ")"); 
                } 
                int len = Math.min(n, mImeBuffer.length() - mCursor); 
                if (len <= 0 || mCursor < 0 || mCursor >= mImeBuffer.length()) { 
                    return ""
                } 
                return mImeBuffer.substring(mCursor, mCursor + len); 
            } 
 
            public CharSequence getTextBeforeCursor(int n, int flags) { 
                if (LOG_IME) { 
                    Log.w(TAG, "getTextBeforeCursor(" + n + "," + flags + ")"); 
                } 
                int len = Math.min(n, mCursor); 
                if (len <= 0 || mCursor < 0 || mCursor >= mImeBuffer.length()) { 
                    return ""
                } 
                return mImeBuffer.substring(mCursor-len, mCursor); 
            } 
 
            public boolean performContextMenuAction(int arg0) { 
                if (LOG_IME) { 
                    Log.w(TAG, "performContextMenuAction" + arg0); 
                } 
                return true
            } 
 
            public boolean performPrivateCommand(String arg0, Bundle arg1) { 
                if (LOG_IME) { 
                    Log.w(TAG, "performPrivateCommand" + arg0 + "," + arg1); 
                } 
                return true
            } 
 
            public boolean reportFullscreenMode(boolean arg0) { 
                if (LOG_IME) { 
                    Log.w(TAG, "reportFullscreenMode" + arg0); 
                } 
                return true
            } 
 
            public boolean commitCorrection (CorrectionInfo correctionInfo) { 
                if (LOG_IME) { 
                    Log.w(TAG, "commitCorrection"); 
                } 
                return true
            } 
 
            public boolean commitText(CharSequence text, int newCursorPosition) { 
                if (LOG_IME) { 
                    Log.w(TAG, "commitText(\"" + text + "\", " + newCursorPosition + ")"); 
                } 
                clearComposingText(); 
                sendText(text); 
                setImeBuffer(""); 
                mCursor = 0
                return true
            } 
 
            private void clearComposingText() { 
                int len = mImeBuffer.length(); 
                if (mComposingTextStart > len || mComposingTextEnd > len) { 
                    mComposingTextEnd = mComposingTextStart = 0
                    return
                } 
                setImeBuffer(mImeBuffer.substring(0, mComposingTextStart) + 
                    mImeBuffer.substring(mComposingTextEnd)); 
                if (mCursor < mComposingTextStart) { 
                    // do nothing 
                } else if (mCursor < mComposingTextEnd) { 
                    mCursor = mComposingTextStart; 
                } else { 
                    mCursor -= mComposingTextEnd - mComposingTextStart; 
                } 
                mComposingTextEnd = mComposingTextStart = 0
            } 
 
            public boolean deleteSurroundingText(int leftLength, int rightLength) { 
                if (LOG_IME) { 
                    Log.w(TAG, "deleteSurroundingText(" + leftLength + 
                            "," + rightLength + ")"); 
                } 
                if (leftLength > 0) { 
                    for (int i = 0; i < leftLength; i++) { 
                        sendKeyEvent( 
                            new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL)); 
                    } 
                } else if ((leftLength == 0) && (rightLength == 0)) { 
                    // Delete key held down / repeating 
                    sendKeyEvent( 
                        new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL)); 
                } 
                // TODO: handle forward deletes. 
                return true
            } 
 
            public boolean performEditorAction(int actionCode) { 
                if (LOG_IME) { 
                    Log.w(TAG, "performEditorAction(" + actionCode + ")"); 
                } 
                if (actionCode == EditorInfo.IME_ACTION_UNSPECIFIED) { 
                    // The "return" key has been pressed on the IME. 
                    sendText("\r"); 
                } 
                return true
            } 
 
            public boolean sendKeyEvent(KeyEvent event) { 
                if (LOG_IME) { 
                    Log.w(TAG, "sendKeyEvent(" + event + ")"); 
                } 
                // Some keys are sent here rather than to commitText. 
                // In particular, del and the digit keys are sent here. 
                // (And I have reports that the HTC Magic also sends Return here.) 
                // As a bit of defensive programming, handle every key. 
                dispatchKeyEvent(event); 
                return true
            } 
 
            public boolean setComposingText(CharSequence text, int newCursorPosition) { 
                if (LOG_IME) { 
                    Log.w(TAG, "setComposingText(\"" + text + "\", " + newCursorPosition + ")"); 
                } 
                int len = mImeBuffer.length(); 
                if (mComposingTextStart > len || mComposingTextEnd > len) { 
                    return false
                } 
                setImeBuffer(mImeBuffer.substring(0, mComposingTextStart) + 
                    text + mImeBuffer.substring(mComposingTextEnd)); 
                mComposingTextEnd = mComposingTextStart + text.length(); 
                mCursor = newCursorPosition > 0 ? mComposingTextEnd + newCursorPosition - 1 
                        : mComposingTextStart - newCursorPosition; 
                return true
            } 
 
            public boolean setSelection(int start, int end) { 
                if (LOG_IME) { 
                    Log.w(TAG, "setSelection" + start + "," + end); 
                } 
                int length = mImeBuffer.length(); 
                if (start == end && start > 0 && start < length) { 
                    mSelectedTextStart = mSelectedTextEnd = 0
                    mCursor = start; 
                } else if (start < end && start > 0 && end < length) { 
                    mSelectedTextStart = start; 
                    mSelectedTextEnd = end; 
                    mCursor = start; 
                } 
                return true
            } 
 
            public boolean setComposingRegion(int start, int end) { 
                if (LOG_IME) { 
                    Log.w(TAG, "setComposingRegion " + start + "," + end); 
                } 
                if (start < end && start > 0 && end < mImeBuffer.length()) { 
                    clearComposingText(); 
                    mComposingTextStart = start; 
                    mComposingTextEnd = end; 
                } 
                return true
            } 
 
            public CharSequence getSelectedText(int flags) { 
                if (LOG_IME) { 
                    Log.w(TAG, "getSelectedText " + flags); 
                } 
                int len = mImeBuffer.length(); 
                if (mSelectedTextEnd >= len || mSelectedTextStart > mSelectedTextEnd) { 
                    return ""
                } 
                return mImeBuffer.substring(mSelectedTextStart, mSelectedTextEnd+1); 
            } 
 
        }; 
    } 
 
    private void setImeBuffer(String buffer) { 
        if (!buffer.equals(mImeBuffer)) { 
            invalidate(); 
        } 
        mImeBuffer = buffer; 
    } 
 
    /**
     * Get the terminal emulator's keypad application mode. 
     */
 
    public boolean getKeypadApplicationMode() { 
        return mEmulator.getKeypadApplicationMode(); 
    } 
 
    /**
     * Set a {@link android.view.GestureDetector.OnGestureListener 
     * GestureDetector.OnGestureListener} to receive gestures performed on this 
     * view.  Can be used to implement additional 
     * functionality via touch gestures or override built-in gestures. 
     * 
     * @param listener The {@link 
     *                 android.view.GestureDetector.OnGestureListener 
     *                 GestureDetector.OnGestureListener} which will receive 
     *                 gestures. 
     */
 
    public void setExtGestureListener(GestureDetector.OnGestureListener listener) { 
        mExtGestureListener = listener; 
    } 
 
    /**
     * Compute the vertical range that the vertical scrollbar represents. 
     */
 
    @Override 
    protected int computeVerticalScrollRange() { 
        return mTranscriptScreen.getActiveRows(); 
    } 
 
    /**
     * Compute the vertical extent of the horizontal scrollbar's thumb within 
     * the vertical range. This value is used to compute the length of the thumb 
     * within the scrollbar's track. 
     */
 
    @Override 
    protected int computeVerticalScrollExtent() { 
        return mRows; 
    } 
 
    /**
     * Compute the vertical offset of the vertical scrollbar's thumb within the 
     * horizontal range. This value is used to compute the position of the thumb 
     * within the scrollbar's track. 
     */
 
    @Override 
    protected int computeVerticalScrollOffset() { 
        return mTranscriptScreen.getActiveRows() + mTopRow - mRows; 
    } 
 
    /**
     * Call this to initialize the view. 
     */
 
    private void initialize() { 
        TermSession session = mTermSession; 
 
        updateText(); 
 
        mTranscriptScreen = session.getTranscriptScreen(); 
        mEmulator = session.getEmulator(); 
        session.setUpdateCallback(mUpdateNotify); 
 
        requestFocus(); 
    } 
 
    /**
     * Get the {@link TermSession} corresponding to this view. 
     * 
     * @return The {@link TermSession} object for this view. 
     */
 
    public TermSession getTermSession() { 
        return mTermSession; 
    } 
 
    /**
     * Get the width of the visible portion of this view. 
     * 
     * @return The width of the visible portion of this view, in pixels. 
     */
 
    public int getVisibleWidth() { 
        return mVisibleWidth; 
    } 
 
    /**
     * Get the height of the visible portion of this view. 
     * 
     * @return The height of the visible portion of this view, in pixels. 
     */
 
    public int getVisibleHeight() { 
        return mVisibleHeight; 
    } 
 
    /**
     * Page the terminal view (scroll it up or down by <code>delta</code> 
     * screenfuls). 
     * 
     * @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 density-independent pixels. 
     */
 
    public void setTextSize(int fontSize) { 
        mTextSize = (int) (fontSize * mDensity); 
        updateText(); 
    } 
 
    /**
     * Sets style information about the cursor. 
     * 
     * @param style The style of the cursor. 
     * @param blink Whether the cursor should blink. 
     */
 
    public void setCursorStyle(int style, int blink) { 
        mCursorStyle = style; 
        if (blink != 0 && mCursorBlink == 0) { 
            mHandler.postDelayed(mBlinkCursor, CURSOR_BLINK_PERIOD); 
        } else if (blink == 0 && mCursorBlink != 0) { 
            mHandler.removeCallbacks(mBlinkCursor); 
        } 
        mCursorBlink = blink; 
    } 
 
    /**
     * Sets the IME mode ("cooked" or "raw"). 
     * 
     * @param useCookedIME Whether the IME should be used in cooked mode. 
     */
 
    public void setUseCookedIME(boolean useCookedIME) { 
        mUseCookedIme = useCookedIME; 
    } 
 
    // Begin GestureDetector.OnGestureListener methods 
 
    public boolean onSingleTapUp(MotionEvent e) { 
        if (mExtGestureListener != null && mExtGestureListener.onSingleTapUp(e)) { 
            return true
        } 
        requestFocus(); 
        return true
    } 
 
    public void onLongPress(MotionEvent e) { 
        // XXX hook into external gesture listener 
        showContextMenu(); 
    } 
 
    public boolean onScroll(MotionEvent e1, MotionEvent e2, 
            float distanceX, float distanceY) { 
        if (mExtGestureListener != null && mExtGestureListener.onScroll(e1, e2, distanceX, distanceY)) { 
            return true
        } 
        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) { 
        if (mExtGestureListener != null && mExtGestureListener.onFling(e1, e2, velocityX, velocityY)) { 
            return true
        } 
        float SCALE = 0.25f
        mScroller.fling(0, mTopRow, 
                -(int) (velocityX * SCALE), -(int) (velocityY * SCALE), 
                00
                -mTranscriptScreen.getActiveTranscriptRows(), 0); 
        mScrollRemainder = 0.0f
        // onScroll(e1, e2, 0.1f * velocityX, -0.1f * velocityY); 
        post(mFlingRunner); 
        return true
    } 
 
    public void onShowPress(MotionEvent e) { 
        if (mExtGestureListener != null) { 
            mExtGestureListener.onShowPress(e); 
        } 
    } 
 
    public boolean onDown(MotionEvent e) { 
        if (mExtGestureListener != null && mExtGestureListener.onDown(e)) { 
            return true
        } 
        mScrollRemainder = 0.0f
        return true
    } 
 
    // End GestureDetector.OnGestureListener methods 
 
    @Override 
    public boolean onTouchEvent(MotionEvent ev) { 
        if (mIsSelectingText) { 
            return onTouchEventWhileSelectingText(ev); 
        } else { 
            return mGestureDetector.onTouchEvent(ev); 
        } 
    } 
 
    @SuppressWarnings("deprecation"
    private boolean onTouchEventWhileSelectingText(MotionEvent ev) { 
        int action = ev.getAction(); 
        int cx = (int)(ev.getX() / mCharacterWidth); 
        int cy = Math.max(0
                (int)((ev.getY() + SELECT_TEXT_OFFSET_Y * mScaledDensity) 
                        / mCharacterHeight) + mTopRow); 
        switch (action) { 
        case MotionEvent.ACTION_DOWN: 
            mSelXAnchor = cx; 
            mSelYAnchor = cy; 
            mSelX1 = cx; 
            mSelY1 = cy; 
            mSelX2 = mSelX1; 
            mSelY2 = mSelY1; 
            break
        case MotionEvent.ACTION_MOVE: 
        case MotionEvent.ACTION_UP: 
            int minx = Math.min(mSelXAnchor, cx); 
            int maxx = Math.max(mSelXAnchor, cx); 
            int miny = Math.min(mSelYAnchor, cy); 
            int maxy = Math.max(mSelYAnchor, cy); 
            mSelX1 = minx; 
            mSelY1 = miny; 
            mSelX2 = maxx; 
            mSelY2 = maxy; 
            if (action == MotionEvent.ACTION_UP) { 
                ClipboardManager clip = (ClipboardManager) 
                     getContext().getApplicationContext() 
                         .getSystemService(Context.CLIPBOARD_SERVICE); 
                clip.setText(getSelectedText().trim()); 
                toggleSelectingText(); 
            } 
            invalidate(); 
            break
        default
            toggleSelectingText(); 
            invalidate(); 
            break
        } 
        return true
    } 
 
    /**
     * Called when a key is pressed in the view. 
     * 
     * @param keyCode The keycode of the key which was pressed. 
     * @param event A {@link KeyEvent} describing the event. 
     * @return Whether the event was handled. 
     */
 
    @Override 
    public boolean onKeyDown(int keyCode, KeyEvent event) { 
        if (LOG_KEY_EVENTS) { 
            Log.w(TAG, "onKeyDown " + keyCode); 
        } 
        if (handleControlKey(keyCode, true)) { 
            return true
        } else if (handleFnKey(keyCode, true)) { 
            return true
        } else if (isSystemKey(keyCode, event)) { 
            if (! isInterceptedSystemKey(keyCode) ) { 
                // Don't intercept the system keys 
                return super.onKeyDown(keyCode, event); 
            } 
        } 
 
        // Translate the keyCode into an ASCII character. 
 
        try { 
            mKeyListener.keyDown(keyCode, event, getKeypadApplicationMode(), 
                    TermKeyListener.isEventFromToggleDevice(event)); 
        } catch (IOException e) { 
            // Ignore I/O exceptions 
        } 
        return true
    } 
 
    /** Do we want to intercept this system key? */ 
    private boolean isInterceptedSystemKey(int keyCode) { 
        return keyCode == KeyEvent.KEYCODE_BACK && mBackKeySendsCharacter; 
    } 
 
    /**
     * Called when a key is released in the view. 
     * 
     * @param keyCode The keycode of the key which was released. 
     * @param event A {@link KeyEvent} describing the event. 
     * @return Whether the event was handled. 
     */
 
    @Override 
    public boolean onKeyUp(int keyCode, KeyEvent event) { 
        if (LOG_KEY_EVENTS) { 
            Log.w(TAG, "onKeyUp " + keyCode); 
        } 
        if (handleControlKey(keyCode, false)) { 
            return true
        } else if (handleFnKey(keyCode, false)) { 
            return true
        } else if (isSystemKey(keyCode, event)) { 
            // Don't intercept the system keys 
            if ( ! isInterceptedSystemKey(keyCode) ) { 
                return super.onKeyUp(keyCode, event); 
            } 
        } 
 
        mKeyListener.keyUp(keyCode, event); 
        clearSpecialKeyStatus(); 
        return true
    } 
 
    @Override 
    public boolean onKeyPreIme(int keyCode, KeyEvent event) { 
        if (sTrapAltAndMeta) { 
            boolean altEsc = mKeyListener.getAltSendsEsc(); 
            boolean altOn = (event.getMetaState() & KeyEvent.META_ALT_ON) != 0
            boolean metaOn = (event.getMetaState() & KeyEvent.META_META_ON) != 0
            boolean altPressed = (keyCode == KeyEvent.KEYCODE_ALT_LEFT) 
                    || (keyCode == KeyEvent.KEYCODE_ALT_RIGHT); 
            boolean altActive = mKeyListener.isAltActive(); 
            if (altEsc && (altOn || altPressed || altActive || metaOn)) { 
                if (event.getAction() == KeyEvent.ACTION_DOWN) { 
                    return onKeyDown(keyCode, event); 
                } else { 
                    return onKeyUp(keyCode, event); 
                } 
            } 
        } 
 
        return super.onKeyPreIme(keyCode, event); 
    }; 
 
    private boolean handleControlKey(int keyCode, boolean down) { 
        if (keyCode == mControlKeyCode) { 
            if (LOG_KEY_EVENTS) { 
                Log.w(TAG, "handleControlKey " + keyCode); 
            } 
            mKeyListener.handleControlKey(down); 
            return true
        } 
        return false
    } 
 
    private boolean handleFnKey(int keyCode, boolean down) { 
        if (keyCode == mFnKeyCode) { 
            if (LOG_KEY_EVENTS) { 
                Log.w(TAG, "handleFnKey " + keyCode); 
            } 
            mKeyListener.handleFnKey(down); 
            return true
        } 
        return false
    } 
 
    private boolean isSystemKey(int keyCode, KeyEvent event) { 
        return event.isSystem(); 
    } 
 
    private void clearSpecialKeyStatus() { 
        if (mIsControlKeySent) { 
            mIsControlKeySent = false
            mKeyListener.handleControlKey(false); 
        } 
        if (mIsFnKeySent) { 
            mIsFnKeySent = false
            mKeyListener.handleFnKey(false); 
        } 
    } 
 
    private void updateText() { 
        ColorScheme scheme = mColorScheme; 
        if (mTextSize > 0) { 
            mTextRenderer = new PaintRenderer(mTextSize, scheme); 
        } 
        else { 
            mTextRenderer = new Bitmap4x8FontRenderer(getResources(), scheme); 
        } 
 
        mForegroundPaint.setColor(scheme.getForeColor()); 
        mBackgroundPaint.setColor(scheme.getBackColor()); 
        mCharacterWidth = mTextRenderer.getCharacterWidth(); 
        mCharacterHeight = mTextRenderer.getCharacterHeight(); 
 
        updateSize(true); 
    } 
 
    /**
     * This is called during layout when the size of this view has changed. If 
     * you were just added to the view hierarchy, you're called with the old 
     * values of 0. 
     */
 
    @Override 
    protected void onSizeChanged(int w, int h, int oldw, int oldh) { 
        if (mTermSession == null) { 
            // Not ready, defer until TermSession is attached 
            mDeferInit = true
            return
        } 
 
        if (!mKnownSize) { 
            mKnownSize = true
            initialize(); 
        } else { 
            updateSize(false); 
        } 
    } 
 
    private void updateSize(int w, int h) { 
        mColumns = Math.max(1, (int) (((float) w) / mCharacterWidth)); 
        mVisibleColumns = (int) (((float) mVisibleWidth) / mCharacterWidth); 
 
        mTopOfScreenMargin = mTextRenderer.getTopMargin(); 
        mRows = Math.max(1, (h - mTopOfScreenMargin) / mCharacterHeight); 
        mTermSession.updateSize(mColumns, mRows); 
 
        // Reset our paging: 
        mTopRow = 0
        mLeftColumn = 0
 
        invalidate(); 
    } 
 
    /**
     * Update the view's idea of its size. 
     * 
     * @param force Whether a size adjustment should be performed even if the 
     *              view's size has not changed. 
     */
 
    public void updateSize(boolean force) { 
        if (mKnownSize) { 
            int w = getWidth(); 
            int h = getHeight(); 
            // Log.w("Term", "(" + w + ", " + h + ")"); 
            if (force || w != mVisibleWidth || h != mVisibleHeight) { 
                mVisibleWidth = w; 
                mVisibleHeight = h; 
                updateSize(mVisibleWidth, mVisibleHeight); 
            } 
        } 
    } 
 
    /**
     * Draw the view to the provided {@link Canvas}. 
     * 
     * @param canvas The {@link Canvas} to draw the view to. 
     */
 
    @Override 
    protected void onDraw(Canvas canvas) { 
        updateSize(false); 
 
        if (mEmulator == null) { 
            // Not ready yet 
            return
        } 
 
        int w = getWidth(); 
        int h = getHeight(); 
 
        boolean reverseVideo = mEmulator.getReverseVideo(); 
        mTextRenderer.setReverseVideo(reverseVideo); 
 
        Paint backgroundPaint = 
                reverseVideo ? mForegroundPaint : mBackgroundPaint; 
        canvas.drawRect(00, w, h, backgroundPaint); 
        float x = -mLeftColumn * mCharacterWidth; 
        float y = mCharacterHeight + mTopOfScreenMargin; 
        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 && mCursorVisible) { 
                cursorX = cx; 
            } 
            int selx1 = -1
            int selx2 = -1
            if ( i >= mSelY1 && i <= mSelY2 ) { 
                if ( i == mSelY1 ) { 
                    selx1 = mSelX1; 
                } 
                if ( i == mSelY2 ) { 
                    selx2 = mSelX2; 
                } else { 
                    selx2 = mColumns; 
                } 
            } 
            mTranscriptScreen.drawText(i, canvas, x, y, mTextRenderer, cursorX, selx1, selx2, mImeBuffer); 
            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
            } 
        } 
    } 
 
    /**
     * Toggle text selection mode in the view. 
     */
 
    public void toggleSelectingText() { 
        mIsSelectingText = ! mIsSelectingText; 
        setVerticalScrollBarEnabled( ! mIsSelectingText ); 
        if ( ! mIsSelectingText ) { 
            mSelX1 = -1
            mSelY1 = -1
            mSelX2 = -1
            mSelY2 = -1
        } 
    } 
 
    /**
     * Whether the view is currently in text selection mode. 
     */
 
    public boolean getSelectingText() { 
        return mIsSelectingText; 
    } 
 
    /**
     * Get selected text. 
     * 
     * @return A {@link String} with the selected text. 
     */
 
    public String getSelectedText() { 
        return mEmulator.getSelectedText(mSelX1, mSelY1, mSelX2, mSelY2); 
    } 
 
    /**
     * Send a Ctrl key event to the terminal. 
     */
 
    public void sendControlKey() { 
        mIsControlKeySent = true
        mKeyListener.handleControlKey(true); 
    } 
 
    /**
     * Send an Fn key event to the terminal.  The Fn modifier key can be used to 
     * generate various special characters and escape codes. 
     */
 
    public void sendFnKey() { 
        mIsFnKeySent = true
        mKeyListener.handleFnKey(true); 
    } 
 
    /**
     * Set the key code to be sent when the Back key is pressed. 
     */
 
    public void setBackKeyCharacter(int keyCode) { 
        mKeyListener.setBackKeyCharacter(keyCode); 
        mBackKeySendsCharacter = (keyCode != 0); 
    } 
 
    /**
     * Set whether to prepend the ESC keycode to the character when when pressing 
     * the ALT Key. 
     * @param flag 
     */
 
    public void setAltSendsEsc(boolean flag) { 
        mKeyListener.setAltSendsEsc(flag); 
    } 
 
    /**
     * Set the keycode corresponding to the Ctrl key. 
     */
 
    public void setControlKeyCode(int keyCode) { 
        mControlKeyCode = keyCode; 
    } 
 
    /**
     * Set the keycode corresponding to the Fn key. 
     */
 
    public void setFnKeyCode(int keyCode) { 
        mFnKeyCode = keyCode; 
    } 
}