0
|
1 /*
|
|
2 * ConnectBot: simple, powerful, open-source SSH client for Android
|
|
3 * Copyright 2007 Kenny Root, Jeffrey Sharkey
|
|
4 *
|
|
5 * Licensed under the Apache License, Version 2.0 (the "License");
|
|
6 * you may not use this file except in compliance with the License.
|
|
7 * You may obtain a copy of the License at
|
|
8 *
|
|
9 * http://www.apache.org/licenses/LICENSE-2.0
|
|
10 *
|
|
11 * Unless required by applicable law or agreed to in writing, software
|
|
12 * distributed under the License is distributed on an "AS IS" BASIS,
|
|
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14 * See the License for the specific language governing permissions and
|
|
15 * limitations under the License.
|
|
16 */
|
|
17
|
|
18 package com.five_ten_sg.connectbot;
|
|
19
|
|
20 import java.util.List;
|
|
21 import java.util.regex.Matcher;
|
|
22 import java.util.regex.Pattern;
|
|
23
|
|
24 import com.five_ten_sg.connectbot.bean.SelectionArea;
|
|
25 import com.five_ten_sg.connectbot.service.FontSizeChangedListener;
|
|
26 import com.five_ten_sg.connectbot.service.TerminalBridge;
|
|
27 import com.five_ten_sg.connectbot.service.TerminalKeyListener;
|
|
28 import android.app.Activity;
|
|
29 import android.content.ContentResolver;
|
|
30 import android.content.Context;
|
|
31 import android.content.Intent;
|
|
32 import android.content.pm.ResolveInfo;
|
|
33 import android.database.Cursor;
|
|
34 import android.graphics.Canvas;
|
|
35 import android.graphics.Matrix;
|
|
36 import android.graphics.Paint;
|
|
37 import android.graphics.Path;
|
|
38 import android.graphics.PixelXorXfermode;
|
|
39 import android.graphics.RectF;
|
|
40 import android.net.Uri;
|
|
41 import android.os.AsyncTask;
|
|
42 import android.view.KeyEvent;
|
|
43 import android.view.View;
|
|
44 import android.view.ViewGroup.LayoutParams;
|
|
45 import android.view.accessibility.AccessibilityEvent;
|
|
46 import android.view.accessibility.AccessibilityManager;
|
|
47 import android.view.inputmethod.BaseInputConnection;
|
|
48 import android.view.inputmethod.EditorInfo;
|
|
49 import android.view.inputmethod.InputConnection;
|
|
50 import android.view.ScaleGestureDetector;
|
|
51 import android.widget.Toast;
|
|
52 import de.mud.terminal.VDUBuffer;
|
|
53
|
|
54 /**
|
|
55 * User interface {@link View} for showing a TerminalBridge in an
|
|
56 * {@link Activity}. Handles drawing bitmap updates and passing keystrokes down
|
|
57 * to terminal.
|
|
58 *
|
|
59 * @author jsharkey
|
|
60 */
|
|
61 public class TerminalView extends View implements FontSizeChangedListener {
|
|
62
|
|
63 private final Context context;
|
|
64 public final TerminalBridge bridge;
|
|
65 private final Paint paint;
|
|
66 private final Paint cursorPaint;
|
|
67 private final Paint cursorStrokePaint;
|
|
68
|
|
69 // Cursor paints to distinguish modes
|
|
70 private Path ctrlCursor, altCursor, shiftCursor;
|
|
71 private RectF tempSrc, tempDst;
|
|
72 private Matrix scaleMatrix;
|
|
73 private static final Matrix.ScaleToFit scaleType = Matrix.ScaleToFit.FILL;
|
|
74
|
|
75 private Toast notification = null;
|
|
76 private String lastNotification = null;
|
|
77 private volatile boolean notifications = true;
|
|
78
|
|
79 // Related to Accessibility Features
|
|
80 private boolean mAccessibilityInitialized = false;
|
|
81 private boolean mAccessibilityActive = true;
|
|
82 private Object[] mAccessibilityLock = new Object[0];
|
|
83 private StringBuffer mAccessibilityBuffer;
|
|
84 private Pattern mControlCodes = null;
|
|
85 private Matcher mCodeMatcher = null;
|
|
86 private AccessibilityEventSender mEventSender = null;
|
|
87
|
23
|
88 public static String android_home_directory = "";
|
|
89
|
0
|
90 private static final String BACKSPACE_CODE = "\\x08\\x1b\\[K";
|
|
91 private static final String CONTROL_CODE_PATTERN = "\\x1b\\[K[^m]+[m|:]";
|
|
92
|
|
93 private static final int ACCESSIBILITY_EVENT_THRESHOLD = 1000;
|
|
94 private static final String SCREENREADER_INTENT_ACTION = "android.accessibilityservice.AccessibilityService";
|
|
95 private static final String SCREENREADER_INTENT_CATEGORY = "android.accessibilityservice.category.FEEDBACK_SPOKEN";
|
|
96
|
|
97 public ScaleGestureDetector mScaleDetector;
|
|
98
|
|
99 public TerminalView(Context context, TerminalBridge bridge) {
|
|
100 super(context);
|
23
|
101 android_home_directory = context.getApplicationInfo().dataDir;
|
0
|
102 this.context = context;
|
|
103 this.bridge = bridge;
|
|
104 paint = new Paint();
|
|
105 setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT));
|
|
106 setFocusable(true);
|
|
107 setFocusableInTouchMode(true);
|
|
108 cursorPaint = new Paint();
|
|
109 cursorPaint.setColor(bridge.color[bridge.defaultFg]);
|
|
110 cursorPaint.setXfermode(new PixelXorXfermode(bridge.color[bridge.defaultBg]));
|
|
111 cursorPaint.setAntiAlias(true);
|
|
112 cursorStrokePaint = new Paint(cursorPaint);
|
|
113 cursorStrokePaint.setStrokeWidth(0.1f);
|
|
114 cursorStrokePaint.setStyle(Paint.Style.STROKE);
|
|
115 /*
|
|
116 * Set up our cursor indicators on a 1x1 Path object which we can later
|
|
117 * transform to our character width and height
|
|
118 */
|
|
119 // TODO make this into a resource somehow
|
|
120 shiftCursor = new Path();
|
|
121 shiftCursor.lineTo(0.5f, 0.33f);
|
|
122 shiftCursor.lineTo(1.0f, 0.0f);
|
|
123 altCursor = new Path();
|
|
124 altCursor.moveTo(0.0f, 1.0f);
|
|
125 altCursor.lineTo(0.5f, 0.66f);
|
|
126 altCursor.lineTo(1.0f, 1.0f);
|
|
127 ctrlCursor = new Path();
|
|
128 ctrlCursor.moveTo(0.0f, 0.25f);
|
|
129 ctrlCursor.lineTo(1.0f, 0.5f);
|
|
130 ctrlCursor.lineTo(0.0f, 0.75f);
|
|
131 // For creating the transform when the terminal resizes
|
|
132 tempSrc = new RectF();
|
|
133 tempSrc.set(0.0f, 0.0f, 1.0f, 1.0f);
|
|
134 tempDst = new RectF();
|
|
135 scaleMatrix = new Matrix();
|
|
136 bridge.addFontSizeChangedListener(this);
|
|
137 // connect our view up to the bridge
|
|
138 setOnKeyListener(bridge.getKeyHandler());
|
|
139 mAccessibilityBuffer = new StringBuffer();
|
|
140 // Enable accessibility features if a screen reader is active.
|
|
141 new AccessibilityStateTester().execute((Void) null);
|
|
142 mScaleDetector = new ScaleGestureDetector(context, new ScaleListener());
|
|
143 }
|
|
144
|
|
145 public void destroy() {
|
|
146 // tell bridge to destroy its bitmap
|
|
147 bridge.parentDestroyed();
|
|
148 }
|
|
149
|
|
150 @Override
|
|
151 protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
|
152 super.onSizeChanged(w, h, oldw, oldh);
|
|
153 bridge.parentChanged(this);
|
|
154 scaleCursors();
|
|
155 }
|
|
156
|
|
157 public void onFontSizeChanged(float size) {
|
|
158 scaleCursors();
|
|
159 }
|
|
160
|
|
161 private void scaleCursors() {
|
|
162 // Create a scale matrix to scale our 1x1 representation of the cursor
|
|
163 tempDst.set(0.0f, 0.0f, bridge.charWidth, bridge.charHeight);
|
|
164 scaleMatrix.setRectToRect(tempSrc, tempDst, scaleType);
|
|
165 }
|
|
166
|
|
167 @Override
|
|
168 public void onDraw(Canvas canvas) {
|
|
169 if (bridge.bitmap != null) {
|
|
170 // draw the bitmap
|
|
171 bridge.onDraw();
|
|
172 // draw the bridge bitmap if it exists
|
|
173 canvas.drawBitmap(bridge.bitmap, 0, 0, paint);
|
|
174
|
|
175 // also draw cursor if visible
|
|
176 if (bridge.buffer.isCursorVisible()) {
|
|
177 int cursorColumn = bridge.buffer.getCursorColumn();
|
|
178 final int cursorRow = bridge.buffer.getCursorRow();
|
|
179 final int columns = bridge.buffer.getColumns();
|
|
180
|
|
181 if (cursorColumn == columns)
|
|
182 cursorColumn = columns - 1;
|
|
183
|
|
184 if (cursorColumn < 0 || cursorRow < 0)
|
|
185 return;
|
|
186
|
|
187 int currentAttribute = bridge.buffer.getAttributes(
|
|
188 cursorColumn, cursorRow);
|
|
189 boolean onWideCharacter = (currentAttribute & VDUBuffer.FULLWIDTH) != 0;
|
|
190 int x = cursorColumn * bridge.charWidth;
|
|
191 int y = (bridge.buffer.getCursorRow()
|
|
192 + bridge.buffer.screenBase - bridge.buffer.windowBase)
|
|
193 * bridge.charHeight;
|
|
194 // Save the current clip and translation
|
|
195 canvas.save();
|
|
196 canvas.translate(x, y);
|
|
197 canvas.clipRect(0, 0,
|
|
198 bridge.charWidth * (onWideCharacter ? 2 : 1),
|
|
199 bridge.charHeight);
|
|
200 canvas.drawPaint(cursorPaint);
|
|
201 final int deadKey = bridge.getKeyHandler().getDeadKey();
|
|
202
|
|
203 if (deadKey != 0) {
|
|
204 canvas.drawText(new char[] { (char)deadKey }, 0, 1, 0, 0, cursorStrokePaint);
|
|
205 }
|
|
206
|
|
207 // Make sure we scale our decorations to the correct size.
|
|
208 canvas.concat(scaleMatrix);
|
|
209 int metaState = bridge.getKeyHandler().getMetaState();
|
|
210
|
|
211 if ((metaState & TerminalKeyListener.META_SHIFT_ON) != 0)
|
|
212 canvas.drawPath(shiftCursor, cursorStrokePaint);
|
|
213 else if ((metaState & TerminalKeyListener.META_SHIFT_LOCK) != 0)
|
|
214 canvas.drawPath(shiftCursor, cursorPaint);
|
|
215
|
|
216 if ((metaState & TerminalKeyListener.META_ALT_ON) != 0)
|
|
217 canvas.drawPath(altCursor, cursorStrokePaint);
|
|
218 else if ((metaState & TerminalKeyListener.META_ALT_LOCK) != 0)
|
|
219 canvas.drawPath(altCursor, cursorPaint);
|
|
220
|
|
221 if ((metaState & TerminalKeyListener.META_CTRL_ON) != 0)
|
|
222 canvas.drawPath(ctrlCursor, cursorStrokePaint);
|
|
223 else if ((metaState & TerminalKeyListener.META_CTRL_LOCK) != 0)
|
|
224 canvas.drawPath(ctrlCursor, cursorPaint);
|
|
225
|
|
226 // Restore previous clip region
|
|
227 canvas.restore();
|
|
228 }
|
|
229
|
|
230 // draw any highlighted area
|
|
231 if (bridge.isSelectingForCopy()) {
|
|
232 SelectionArea area = bridge.getSelectionArea();
|
|
233 canvas.save(Canvas.CLIP_SAVE_FLAG);
|
|
234 canvas.clipRect(
|
|
235 area.getLeft() * bridge.charWidth,
|
|
236 area.getTop() * bridge.charHeight,
|
|
237 (area.getRight() + 1) * bridge.charWidth,
|
|
238 (area.getBottom() + 1) * bridge.charHeight
|
|
239 );
|
|
240 canvas.drawPaint(cursorPaint);
|
|
241 canvas.restore();
|
|
242 }
|
|
243 }
|
|
244 }
|
|
245
|
|
246 public void notifyUser(String message) {
|
|
247 if (!notifications)
|
|
248 return;
|
|
249
|
|
250 if (notification != null) {
|
|
251 // Don't keep telling the user the same thing.
|
|
252 if (lastNotification != null && lastNotification.equals(message))
|
|
253 return;
|
|
254
|
|
255 notification.setText(message);
|
|
256 notification.show();
|
|
257 }
|
|
258 else {
|
|
259 notification = Toast.makeText(context, message, Toast.LENGTH_SHORT);
|
|
260 notification.show();
|
|
261 }
|
|
262
|
|
263 lastNotification = message;
|
|
264 }
|
|
265
|
|
266 /**
|
|
267 * Ask the {@link TerminalBridge} we're connected to to resize to a specific size.
|
|
268 * @param width
|
|
269 * @param height
|
|
270 */
|
|
271 public void forceSize(int width, int height) {
|
|
272 bridge.resizeComputed(width, height, getWidth(), getHeight());
|
|
273 }
|
|
274
|
|
275 /**
|
|
276 * Sets the ability for the TerminalView to display Toast notifications to the user.
|
|
277 * @param value whether to enable notifications or not
|
|
278 */
|
|
279 public void setNotifications(boolean value) {
|
|
280 notifications = value;
|
|
281 }
|
|
282
|
|
283 @Override
|
|
284 public boolean onCheckIsTextEditor() {
|
|
285 return true;
|
|
286 }
|
|
287
|
|
288 @Override
|
|
289 public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
|
|
290 outAttrs.imeOptions |=
|
|
291 EditorInfo.IME_FLAG_NO_EXTRACT_UI |
|
|
292 EditorInfo.IME_FLAG_NO_ENTER_ACTION |
|
|
293 EditorInfo.IME_ACTION_NONE;
|
|
294 outAttrs.inputType = EditorInfo.TYPE_NULL;
|
|
295 return new BaseInputConnection(this, false) {
|
|
296 @Override
|
|
297 public boolean deleteSurroundingText(int leftLength, int rightLength) {
|
|
298 if (rightLength == 0 && leftLength == 0) {
|
|
299 return this.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL));
|
|
300 }
|
|
301
|
|
302 for (int i = 0; i < leftLength; i++) {
|
|
303 this.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL));
|
|
304 }
|
|
305
|
|
306 // TODO: forward delete
|
|
307 return true;
|
|
308 }
|
|
309 };
|
|
310 }
|
|
311
|
|
312 public void propagateConsoleText(char[] rawText, int length) {
|
|
313 if (mAccessibilityActive) {
|
|
314 synchronized (mAccessibilityLock) {
|
|
315 mAccessibilityBuffer.append(rawText, 0, length);
|
|
316 }
|
|
317
|
|
318 if (mAccessibilityInitialized) {
|
|
319 if (mEventSender != null) {
|
|
320 removeCallbacks(mEventSender);
|
|
321 }
|
|
322 else {
|
|
323 mEventSender = new AccessibilityEventSender();
|
|
324 }
|
|
325
|
|
326 postDelayed(mEventSender, ACCESSIBILITY_EVENT_THRESHOLD);
|
|
327 }
|
|
328 }
|
|
329 }
|
|
330
|
|
331 private class AccessibilityEventSender implements Runnable {
|
|
332 public void run() {
|
|
333 synchronized (mAccessibilityLock) {
|
|
334 if (mCodeMatcher == null) {
|
|
335 mCodeMatcher = mControlCodes.matcher(mAccessibilityBuffer);
|
|
336 }
|
|
337 else {
|
|
338 mCodeMatcher.reset(mAccessibilityBuffer);
|
|
339 }
|
|
340
|
|
341 // Strip all control codes out.
|
|
342 mAccessibilityBuffer = new StringBuffer(mCodeMatcher.replaceAll(" "));
|
|
343 // Apply Backspaces using backspace character sequence
|
|
344 int i = mAccessibilityBuffer.indexOf(BACKSPACE_CODE);
|
|
345
|
|
346 while (i != -1) {
|
|
347 mAccessibilityBuffer = mAccessibilityBuffer.replace(i == 0 ? 0 : i - 1, i
|
|
348 + BACKSPACE_CODE.length(), "");
|
|
349 i = mAccessibilityBuffer.indexOf(BACKSPACE_CODE);
|
|
350 }
|
|
351
|
|
352 if (mAccessibilityBuffer.length() > 0) {
|
|
353 AccessibilityEvent event = AccessibilityEvent.obtain(
|
|
354 AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED);
|
|
355 event.setFromIndex(0);
|
|
356 event.setAddedCount(mAccessibilityBuffer.length());
|
|
357 event.getText().add(mAccessibilityBuffer);
|
|
358 sendAccessibilityEventUnchecked(event);
|
|
359 mAccessibilityBuffer.setLength(0);
|
|
360 }
|
|
361 }
|
|
362 }
|
|
363 }
|
|
364
|
|
365 private class AccessibilityStateTester extends AsyncTask<Void, Void, Boolean> {
|
|
366 @Override
|
|
367 protected Boolean doInBackground(Void... params) {
|
|
368 /*
|
|
369 * Presumably if the accessibility manager is not enabled, we don't
|
|
370 * need to send accessibility events.
|
|
371 */
|
|
372 final AccessibilityManager accessibility = (AccessibilityManager) context
|
|
373 .getSystemService(Context.ACCESSIBILITY_SERVICE);
|
|
374
|
|
375 if (!accessibility.isEnabled()) {
|
|
376 return false;
|
|
377 }
|
|
378
|
|
379 /*
|
|
380 * Restrict the set of intents to only accessibility services that
|
|
381 * have the category FEEDBACK_SPOKEN (aka, screen readers).
|
|
382 */
|
|
383 final Intent screenReaderIntent = new Intent(SCREENREADER_INTENT_ACTION);
|
|
384 screenReaderIntent.addCategory(SCREENREADER_INTENT_CATEGORY);
|
|
385 final ContentResolver cr = context.getContentResolver();
|
|
386 final List<ResolveInfo> screenReaders = context.getPackageManager().queryIntentServices(
|
|
387 screenReaderIntent, 0);
|
|
388 boolean foundScreenReader = false;
|
|
389 final int N = screenReaders.size();
|
|
390
|
|
391 for (int i = 0; i < N; i++) {
|
|
392 final ResolveInfo screenReader = screenReaders.get(i);
|
|
393 /*
|
|
394 * All screen readers are expected to implement a content
|
|
395 * provider that responds to:
|
|
396 * content://<nameofpackage>.providers.StatusProvider
|
|
397 */
|
|
398 final Cursor cursor = cr.query(
|
|
399 Uri.parse("content://" + screenReader.serviceInfo.packageName
|
|
400 + ".providers.StatusProvider"), null, null, null, null);
|
|
401
|
|
402 if (cursor != null && cursor.moveToFirst()) {
|
|
403 /*
|
|
404 * These content providers use a special cursor that only has
|
|
405 * one element, an integer that is 1 if the screen reader is
|
|
406 * running.
|
|
407 */
|
|
408 final int status = cursor.getInt(0);
|
|
409 cursor.close();
|
|
410
|
|
411 if (status == 1) {
|
|
412 foundScreenReader = true;
|
|
413 break;
|
|
414 }
|
|
415 }
|
|
416 }
|
|
417
|
|
418 if (foundScreenReader) {
|
|
419 mControlCodes = Pattern.compile(CONTROL_CODE_PATTERN);
|
|
420 }
|
|
421
|
|
422 return foundScreenReader;
|
|
423 }
|
|
424
|
|
425 @Override
|
|
426 protected void onPostExecute(Boolean result) {
|
|
427 mAccessibilityActive = result;
|
|
428 mAccessibilityInitialized = true;
|
|
429
|
|
430 if (result) {
|
|
431 mEventSender = new AccessibilityEventSender();
|
|
432 postDelayed(mEventSender, ACCESSIBILITY_EVENT_THRESHOLD);
|
|
433 }
|
|
434 else {
|
|
435 mAccessibilityBuffer = null;
|
|
436 }
|
|
437 }
|
|
438 }
|
|
439
|
|
440 private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
|
|
441 @Override
|
|
442 public boolean onScale(ScaleGestureDetector detector) {
|
|
443 float mScaleFactor = detector.getScaleFactor();
|
|
444
|
|
445 if (mScaleFactor > 1.1) {
|
|
446 bridge.increaseFontSize();
|
|
447 return true;
|
|
448 }
|
|
449 else if (mScaleFactor < 0.9) {
|
|
450 bridge.decreaseFontSize();
|
|
451 return true;
|
|
452 }
|
|
453
|
|
454 return (false);
|
|
455 }
|
|
456 }
|
|
457
|
|
458 }
|