Mercurial > 510Connectbot
view app/src/main/java/com/five_ten_sg/connectbot/TerminalView.java @ 513:13a922d1fc5c
update docs
author | Carl Byington <carl@five-ten-sg.com> |
---|---|
date | Fri, 10 Feb 2023 17:16:47 -0700 |
parents | 105815cce146 |
children |
line wrap: on
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.ColorMatrix; import android.graphics.ColorMatrixColorFilter; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Path; import android.graphics.RectF; import android.net.Uri; import android.os.AsyncTask; import android.util.Log; import android.view.KeyEvent; import android.view.ScaleGestureDetector; 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.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 { public final static String TAG = "ConnectBot.TerminalView"; 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; private ColorMatrix getColorMatrix() { return new ColorMatrix(new float[] { -1, 0, 0, 0, 255, 0, -1, 0, 0, 255, 0, 0, -1, 0, 255, 0, 0, 0, 1, 0 }); } 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.setColorFilter(new ColorMatrixColorFilter(getColorMatrix())); //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); canvas.restore(); // Restore previous clip region } // draw any highlighted area if (bridge.isSelectingForCopy()) { SelectionArea area = bridge.getSelectionArea(); canvas.save(); 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) { try { // ignore if don't want notifications if (!notifications) return; // Don't keep telling the user the same thing. if (lastNotification != null && lastNotification.equals(message)) return; // create or use old toast if (notification == null) { notification = Toast.makeText(context, message, Toast.LENGTH_SHORT); } else { notification.setText(message); } notification.show(); lastNotification = message; } catch (Exception e) { Log.e(TAG, "Problem while trying to notify user", e); } } /** * 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); } } }