line source
+ − /*
+ − * ConnectBot: simple, powerful, open-source SSH client for Android
+ − * Copyright 2007 Kenny Root, Jeffrey Sharkey
+ − *
+ − * 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.five_ten_sg.connectbot;
+ −
+ − import java.util.List;
+ − import java.util.regex.Matcher;
+ − import java.util.regex.Pattern;
+ −
+ − import com.five_ten_sg.connectbot.bean.SelectionArea;
+ − import com.five_ten_sg.connectbot.service.FontSizeChangedListener;
+ − import com.five_ten_sg.connectbot.service.TerminalBridge;
+ − import com.five_ten_sg.connectbot.service.TerminalKeyListener;
+ − import android.app.Activity;
+ − import android.content.ContentResolver;
+ − import android.content.Context;
+ − import android.content.Intent;
+ − import android.content.pm.ResolveInfo;
+ − import android.database.Cursor;
+ − import android.graphics.Canvas;
+ − import android.graphics.Matrix;
+ − import android.graphics.Paint;
+ − import android.graphics.Path;
+ − import android.graphics.PixelXorXfermode;
+ − import android.graphics.RectF;
+ − import android.net.Uri;
+ − import android.os.AsyncTask;
+ − import android.view.KeyEvent;
+ − import android.view.View;
+ − import android.view.ViewGroup.LayoutParams;
+ − import android.view.accessibility.AccessibilityEvent;
+ − import android.view.accessibility.AccessibilityManager;
+ − import android.view.inputmethod.BaseInputConnection;
+ − import android.view.inputmethod.EditorInfo;
+ − import android.view.inputmethod.InputConnection;
+ − import android.view.ScaleGestureDetector;
+ − import android.widget.Toast;
+ − import de.mud.terminal.VDUBuffer;
+ −
+ − /**
+ − * User interface {@link View} for showing a TerminalBridge in an
+ − * {@link Activity}. Handles drawing bitmap updates and passing keystrokes down
+ − * to terminal.
+ − *
+ − * @author jsharkey
+ − */
+ − public class TerminalView extends View implements FontSizeChangedListener {
+ −
+ − private final Context context;
+ − public final TerminalBridge bridge;
+ − private final Paint paint;
+ − private final Paint cursorPaint;
+ − private final Paint cursorStrokePaint;
+ −
+ − // Cursor paints to distinguish modes
+ − private Path ctrlCursor, altCursor, shiftCursor;
+ − private RectF tempSrc, tempDst;
+ − private Matrix scaleMatrix;
+ − private static final Matrix.ScaleToFit scaleType = Matrix.ScaleToFit.FILL;
+ −
+ − private Toast notification = null;
+ − private String lastNotification = null;
+ − private volatile boolean notifications = true;
+ −
+ − // Related to Accessibility Features
+ − private boolean mAccessibilityInitialized = false;
+ − private boolean mAccessibilityActive = true;
+ − private Object[] mAccessibilityLock = new Object[0];
+ − private StringBuffer mAccessibilityBuffer;
+ − private Pattern mControlCodes = null;
+ − private Matcher mCodeMatcher = null;
+ − private AccessibilityEventSender mEventSender = null;
+ −
+ −
+ − private static final String BACKSPACE_CODE = "\\x08\\x1b\\[K";
+ − private static final String CONTROL_CODE_PATTERN = "\\x1b\\[K[^m]+[m|:]";
+ −
+ − private static final int ACCESSIBILITY_EVENT_THRESHOLD = 1000;
+ − private static final String SCREENREADER_INTENT_ACTION = "android.accessibilityservice.AccessibilityService";
+ − private static final String SCREENREADER_INTENT_CATEGORY = "android.accessibilityservice.category.FEEDBACK_SPOKEN";
+ −
+ − public ScaleGestureDetector mScaleDetector;
+ −
+ − public TerminalView(Context context, TerminalBridge bridge) {
+ − super(context);
+ − this.context = context;
+ − this.bridge = bridge;
+ − paint = new Paint();
+ − setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT));
+ − setFocusable(true);
+ − setFocusableInTouchMode(true);
+ − cursorPaint = new Paint();
+ − cursorPaint.setColor(bridge.color[bridge.defaultFg]);
+ − cursorPaint.setXfermode(new PixelXorXfermode(bridge.color[bridge.defaultBg]));
+ − cursorPaint.setAntiAlias(true);
+ − cursorStrokePaint = new Paint(cursorPaint);
+ − cursorStrokePaint.setStrokeWidth(0.1f);
+ − cursorStrokePaint.setStyle(Paint.Style.STROKE);
+ − /*
+ − * Set up our cursor indicators on a 1x1 Path object which we can later
+ − * transform to our character width and height
+ − */
+ − // TODO make this into a resource somehow
+ − shiftCursor = new Path();
+ − shiftCursor.lineTo(0.5f, 0.33f);
+ − shiftCursor.lineTo(1.0f, 0.0f);
+ − altCursor = new Path();
+ − altCursor.moveTo(0.0f, 1.0f);
+ − altCursor.lineTo(0.5f, 0.66f);
+ − altCursor.lineTo(1.0f, 1.0f);
+ − ctrlCursor = new Path();
+ − ctrlCursor.moveTo(0.0f, 0.25f);
+ − ctrlCursor.lineTo(1.0f, 0.5f);
+ − ctrlCursor.lineTo(0.0f, 0.75f);
+ − // For creating the transform when the terminal resizes
+ − tempSrc = new RectF();
+ − tempSrc.set(0.0f, 0.0f, 1.0f, 1.0f);
+ − tempDst = new RectF();
+ − scaleMatrix = new Matrix();
+ − bridge.addFontSizeChangedListener(this);
+ − // connect our view up to the bridge
+ − setOnKeyListener(bridge.getKeyHandler());
+ − mAccessibilityBuffer = new StringBuffer();
+ − // Enable accessibility features if a screen reader is active.
+ − new AccessibilityStateTester().execute((Void) null);
+ − mScaleDetector = new ScaleGestureDetector(context, new ScaleListener());
+ − }
+ −
+ − public void destroy() {
+ − // tell bridge to destroy its bitmap
+ − bridge.parentDestroyed();
+ − }
+ −
+ − @Override
+ − protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ − super.onSizeChanged(w, h, oldw, oldh);
+ − bridge.parentChanged(this);
+ − scaleCursors();
+ − }
+ −
+ − public void onFontSizeChanged(float size) {
+ − scaleCursors();
+ − }
+ −
+ − private void scaleCursors() {
+ − // Create a scale matrix to scale our 1x1 representation of the cursor
+ − tempDst.set(0.0f, 0.0f, bridge.charWidth, bridge.charHeight);
+ − scaleMatrix.setRectToRect(tempSrc, tempDst, scaleType);
+ − }
+ −
+ − @Override
+ − public void onDraw(Canvas canvas) {
+ − if (bridge.bitmap != null) {
+ − // draw the bitmap
+ − bridge.onDraw();
+ − // draw the bridge bitmap if it exists
+ − canvas.drawBitmap(bridge.bitmap, 0, 0, paint);
+ −
+ − // also draw cursor if visible
+ − if (bridge.buffer.isCursorVisible()) {
+ − int cursorColumn = bridge.buffer.getCursorColumn();
+ − final int cursorRow = bridge.buffer.getCursorRow();
+ − final int columns = bridge.buffer.getColumns();
+ −
+ − if (cursorColumn == columns)
+ − cursorColumn = columns - 1;
+ −
+ − if (cursorColumn < 0 || cursorRow < 0)
+ − return;
+ −
+ − int currentAttribute = bridge.buffer.getAttributes(
+ − cursorColumn, cursorRow);
+ − boolean onWideCharacter = (currentAttribute & VDUBuffer.FULLWIDTH) != 0;
+ − int x = cursorColumn * bridge.charWidth;
+ − int y = (bridge.buffer.getCursorRow()
+ − + bridge.buffer.screenBase - bridge.buffer.windowBase)
+ − * bridge.charHeight;
+ − // Save the current clip and translation
+ − canvas.save();
+ − canvas.translate(x, y);
+ − canvas.clipRect(0, 0,
+ − bridge.charWidth * (onWideCharacter ? 2 : 1),
+ − bridge.charHeight);
+ − canvas.drawPaint(cursorPaint);
+ − final int deadKey = bridge.getKeyHandler().getDeadKey();
+ −
+ − if (deadKey != 0) {
+ − canvas.drawText(new char[] { (char)deadKey }, 0, 1, 0, 0, cursorStrokePaint);
+ − }
+ −
+ − // Make sure we scale our decorations to the correct size.
+ − canvas.concat(scaleMatrix);
+ − int metaState = bridge.getKeyHandler().getMetaState();
+ −
+ − if ((metaState & TerminalKeyListener.META_SHIFT_ON) != 0)
+ − canvas.drawPath(shiftCursor, cursorStrokePaint);
+ − else if ((metaState & TerminalKeyListener.META_SHIFT_LOCK) != 0)
+ − canvas.drawPath(shiftCursor, cursorPaint);
+ −
+ − if ((metaState & TerminalKeyListener.META_ALT_ON) != 0)
+ − canvas.drawPath(altCursor, cursorStrokePaint);
+ − else if ((metaState & TerminalKeyListener.META_ALT_LOCK) != 0)
+ − canvas.drawPath(altCursor, cursorPaint);
+ −
+ − if ((metaState & TerminalKeyListener.META_CTRL_ON) != 0)
+ − canvas.drawPath(ctrlCursor, cursorStrokePaint);
+ − else if ((metaState & TerminalKeyListener.META_CTRL_LOCK) != 0)
+ − canvas.drawPath(ctrlCursor, cursorPaint);
+ −
+ − // Restore previous clip region
+ − canvas.restore();
+ − }
+ −
+ − // draw any highlighted area
+ − if (bridge.isSelectingForCopy()) {
+ − SelectionArea area = bridge.getSelectionArea();
+ − canvas.save(Canvas.CLIP_SAVE_FLAG);
+ − canvas.clipRect(
+ − area.getLeft() * bridge.charWidth,
+ − area.getTop() * bridge.charHeight,
+ − (area.getRight() + 1) * bridge.charWidth,
+ − (area.getBottom() + 1) * bridge.charHeight
+ − );
+ − canvas.drawPaint(cursorPaint);
+ − canvas.restore();
+ − }
+ − }
+ − }
+ −
+ − public void notifyUser(String message) {
+ − if (!notifications)
+ − return;
+ −
+ − if (notification != null) {
+ − // Don't keep telling the user the same thing.
+ − if (lastNotification != null && lastNotification.equals(message))
+ − return;
+ −
+ − notification.setText(message);
+ − notification.show();
+ − }
+ − else {
+ − notification = Toast.makeText(context, message, Toast.LENGTH_SHORT);
+ − notification.show();
+ − }
+ −
+ − lastNotification = message;
+ − }
+ −
+ − /**
+ − * Ask the {@link TerminalBridge} we're connected to to resize to a specific size.
+ − * @param width
+ − * @param height
+ − */
+ − public void forceSize(int width, int height) {
+ − bridge.resizeComputed(width, height, getWidth(), getHeight());
+ − }
+ −
+ − /**
+ − * Sets the ability for the TerminalView to display Toast notifications to the user.
+ − * @param value whether to enable notifications or not
+ − */
+ − public void setNotifications(boolean value) {
+ − notifications = value;
+ − }
+ −
+ − @Override
+ − public boolean onCheckIsTextEditor() {
+ − return true;
+ − }
+ −
+ − @Override
+ − public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
+ − outAttrs.imeOptions |=
+ − EditorInfo.IME_FLAG_NO_EXTRACT_UI |
+ − EditorInfo.IME_FLAG_NO_ENTER_ACTION |
+ − EditorInfo.IME_ACTION_NONE;
+ − outAttrs.inputType = EditorInfo.TYPE_NULL;
+ − return new BaseInputConnection(this, false) {
+ − @Override
+ − public boolean deleteSurroundingText(int leftLength, int rightLength) {
+ − if (rightLength == 0 && leftLength == 0) {
+ − return this.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL));
+ − }
+ −
+ − for (int i = 0; i < leftLength; i++) {
+ − this.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL));
+ − }
+ −
+ − // TODO: forward delete
+ − return true;
+ − }
+ − };
+ − }
+ −
+ − public void propagateConsoleText(char[] rawText, int length) {
+ − if (mAccessibilityActive) {
+ − synchronized (mAccessibilityLock) {
+ − mAccessibilityBuffer.append(rawText, 0, length);
+ − }
+ −
+ − if (mAccessibilityInitialized) {
+ − if (mEventSender != null) {
+ − removeCallbacks(mEventSender);
+ − }
+ − else {
+ − mEventSender = new AccessibilityEventSender();
+ − }
+ −
+ − postDelayed(mEventSender, ACCESSIBILITY_EVENT_THRESHOLD);
+ − }
+ − }
+ − }
+ −
+ − private class AccessibilityEventSender implements Runnable {
+ − public void run() {
+ − synchronized (mAccessibilityLock) {
+ − if (mCodeMatcher == null) {
+ − mCodeMatcher = mControlCodes.matcher(mAccessibilityBuffer);
+ − }
+ − else {
+ − mCodeMatcher.reset(mAccessibilityBuffer);
+ − }
+ −
+ − // Strip all control codes out.
+ − mAccessibilityBuffer = new StringBuffer(mCodeMatcher.replaceAll(" "));
+ − // Apply Backspaces using backspace character sequence
+ − int i = mAccessibilityBuffer.indexOf(BACKSPACE_CODE);
+ −
+ − while (i != -1) {
+ − mAccessibilityBuffer = mAccessibilityBuffer.replace(i == 0 ? 0 : i - 1, i
+ − + BACKSPACE_CODE.length(), "");
+ − i = mAccessibilityBuffer.indexOf(BACKSPACE_CODE);
+ − }
+ −
+ − if (mAccessibilityBuffer.length() > 0) {
+ − AccessibilityEvent event = AccessibilityEvent.obtain(
+ − AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED);
+ − event.setFromIndex(0);
+ − event.setAddedCount(mAccessibilityBuffer.length());
+ − event.getText().add(mAccessibilityBuffer);
+ − sendAccessibilityEventUnchecked(event);
+ − mAccessibilityBuffer.setLength(0);
+ − }
+ − }
+ − }
+ − }
+ −
+ − private class AccessibilityStateTester extends AsyncTask<Void, Void, Boolean> {
+ − @Override
+ − protected Boolean doInBackground(Void... params) {
+ − /*
+ − * Presumably if the accessibility manager is not enabled, we don't
+ − * need to send accessibility events.
+ − */
+ − final AccessibilityManager accessibility = (AccessibilityManager) context
+ − .getSystemService(Context.ACCESSIBILITY_SERVICE);
+ −
+ − if (!accessibility.isEnabled()) {
+ − return false;
+ − }
+ −
+ − /*
+ − * Restrict the set of intents to only accessibility services that
+ − * have the category FEEDBACK_SPOKEN (aka, screen readers).
+ − */
+ − final Intent screenReaderIntent = new Intent(SCREENREADER_INTENT_ACTION);
+ − screenReaderIntent.addCategory(SCREENREADER_INTENT_CATEGORY);
+ − final ContentResolver cr = context.getContentResolver();
+ − final List<ResolveInfo> screenReaders = context.getPackageManager().queryIntentServices(
+ − screenReaderIntent, 0);
+ − boolean foundScreenReader = false;
+ − final int N = screenReaders.size();
+ −
+ − for (int i = 0; i < N; i++) {
+ − final ResolveInfo screenReader = screenReaders.get(i);
+ − /*
+ − * All screen readers are expected to implement a content
+ − * provider that responds to:
+ − * content://<nameofpackage>.providers.StatusProvider
+ − */
+ − final Cursor cursor = cr.query(
+ − Uri.parse("content://" + screenReader.serviceInfo.packageName
+ − + ".providers.StatusProvider"), null, null, null, null);
+ −
+ − if (cursor != null && cursor.moveToFirst()) {
+ − /*
+ − * These content providers use a special cursor that only has
+ − * one element, an integer that is 1 if the screen reader is
+ − * running.
+ − */
+ − final int status = cursor.getInt(0);
+ − cursor.close();
+ −
+ − if (status == 1) {
+ − foundScreenReader = true;
+ − break;
+ − }
+ − }
+ − }
+ −
+ − if (foundScreenReader) {
+ − mControlCodes = Pattern.compile(CONTROL_CODE_PATTERN);
+ − }
+ −
+ − return foundScreenReader;
+ − }
+ −
+ − @Override
+ − protected void onPostExecute(Boolean result) {
+ − mAccessibilityActive = result;
+ − mAccessibilityInitialized = true;
+ −
+ − if (result) {
+ − mEventSender = new AccessibilityEventSender();
+ − postDelayed(mEventSender, ACCESSIBILITY_EVENT_THRESHOLD);
+ − }
+ − else {
+ − mAccessibilityBuffer = null;
+ − }
+ − }
+ − }
+ −
+ − private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
+ − @Override
+ − public boolean onScale(ScaleGestureDetector detector) {
+ − float mScaleFactor = detector.getScaleFactor();
+ −
+ − if (mScaleFactor > 1.1) {
+ − bridge.increaseFontSize();
+ − return true;
+ − }
+ − else if (mScaleFactor < 0.9) {
+ − bridge.decreaseFontSize();
+ − return true;
+ − }
+ −
+ − return (false);
+ − }
+ − }
+ −
+ − }