comparison src/com/five_ten_sg/connectbot/TerminalView.java @ 0:0ce5cc452d02

initial version
author Carl Byington <carl@five-ten-sg.com>
date Thu, 22 May 2014 10:41:19 -0700
parents
children cfcb8d9859a8
comparison
equal deleted inserted replaced
-1:000000000000 0:0ce5cc452d02
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
88 private static final String BACKSPACE_CODE = "\\x08\\x1b\\[K";
89 private static final String CONTROL_CODE_PATTERN = "\\x1b\\[K[^m]+[m|:]";
90
91 private static final int ACCESSIBILITY_EVENT_THRESHOLD = 1000;
92 private static final String SCREENREADER_INTENT_ACTION = "android.accessibilityservice.AccessibilityService";
93 private static final String SCREENREADER_INTENT_CATEGORY = "android.accessibilityservice.category.FEEDBACK_SPOKEN";
94
95 public ScaleGestureDetector mScaleDetector;
96
97 public TerminalView(Context context, TerminalBridge bridge) {
98 super(context);
99 this.context = context;
100 this.bridge = bridge;
101 paint = new Paint();
102 setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT));
103 setFocusable(true);
104 setFocusableInTouchMode(true);
105 cursorPaint = new Paint();
106 cursorPaint.setColor(bridge.color[bridge.defaultFg]);
107 cursorPaint.setXfermode(new PixelXorXfermode(bridge.color[bridge.defaultBg]));
108 cursorPaint.setAntiAlias(true);
109 cursorStrokePaint = new Paint(cursorPaint);
110 cursorStrokePaint.setStrokeWidth(0.1f);
111 cursorStrokePaint.setStyle(Paint.Style.STROKE);
112 /*
113 * Set up our cursor indicators on a 1x1 Path object which we can later
114 * transform to our character width and height
115 */
116 // TODO make this into a resource somehow
117 shiftCursor = new Path();
118 shiftCursor.lineTo(0.5f, 0.33f);
119 shiftCursor.lineTo(1.0f, 0.0f);
120 altCursor = new Path();
121 altCursor.moveTo(0.0f, 1.0f);
122 altCursor.lineTo(0.5f, 0.66f);
123 altCursor.lineTo(1.0f, 1.0f);
124 ctrlCursor = new Path();
125 ctrlCursor.moveTo(0.0f, 0.25f);
126 ctrlCursor.lineTo(1.0f, 0.5f);
127 ctrlCursor.lineTo(0.0f, 0.75f);
128 // For creating the transform when the terminal resizes
129 tempSrc = new RectF();
130 tempSrc.set(0.0f, 0.0f, 1.0f, 1.0f);
131 tempDst = new RectF();
132 scaleMatrix = new Matrix();
133 bridge.addFontSizeChangedListener(this);
134 // connect our view up to the bridge
135 setOnKeyListener(bridge.getKeyHandler());
136 mAccessibilityBuffer = new StringBuffer();
137 // Enable accessibility features if a screen reader is active.
138 new AccessibilityStateTester().execute((Void) null);
139 mScaleDetector = new ScaleGestureDetector(context, new ScaleListener());
140 }
141
142 public void destroy() {
143 // tell bridge to destroy its bitmap
144 bridge.parentDestroyed();
145 }
146
147 @Override
148 protected void onSizeChanged(int w, int h, int oldw, int oldh) {
149 super.onSizeChanged(w, h, oldw, oldh);
150 bridge.parentChanged(this);
151 scaleCursors();
152 }
153
154 public void onFontSizeChanged(float size) {
155 scaleCursors();
156 }
157
158 private void scaleCursors() {
159 // Create a scale matrix to scale our 1x1 representation of the cursor
160 tempDst.set(0.0f, 0.0f, bridge.charWidth, bridge.charHeight);
161 scaleMatrix.setRectToRect(tempSrc, tempDst, scaleType);
162 }
163
164 @Override
165 public void onDraw(Canvas canvas) {
166 if (bridge.bitmap != null) {
167 // draw the bitmap
168 bridge.onDraw();
169 // draw the bridge bitmap if it exists
170 canvas.drawBitmap(bridge.bitmap, 0, 0, paint);
171
172 // also draw cursor if visible
173 if (bridge.buffer.isCursorVisible()) {
174 int cursorColumn = bridge.buffer.getCursorColumn();
175 final int cursorRow = bridge.buffer.getCursorRow();
176 final int columns = bridge.buffer.getColumns();
177
178 if (cursorColumn == columns)
179 cursorColumn = columns - 1;
180
181 if (cursorColumn < 0 || cursorRow < 0)
182 return;
183
184 int currentAttribute = bridge.buffer.getAttributes(
185 cursorColumn, cursorRow);
186 boolean onWideCharacter = (currentAttribute & VDUBuffer.FULLWIDTH) != 0;
187 int x = cursorColumn * bridge.charWidth;
188 int y = (bridge.buffer.getCursorRow()
189 + bridge.buffer.screenBase - bridge.buffer.windowBase)
190 * bridge.charHeight;
191 // Save the current clip and translation
192 canvas.save();
193 canvas.translate(x, y);
194 canvas.clipRect(0, 0,
195 bridge.charWidth * (onWideCharacter ? 2 : 1),
196 bridge.charHeight);
197 canvas.drawPaint(cursorPaint);
198 final int deadKey = bridge.getKeyHandler().getDeadKey();
199
200 if (deadKey != 0) {
201 canvas.drawText(new char[] { (char)deadKey }, 0, 1, 0, 0, cursorStrokePaint);
202 }
203
204 // Make sure we scale our decorations to the correct size.
205 canvas.concat(scaleMatrix);
206 int metaState = bridge.getKeyHandler().getMetaState();
207
208 if ((metaState & TerminalKeyListener.META_SHIFT_ON) != 0)
209 canvas.drawPath(shiftCursor, cursorStrokePaint);
210 else if ((metaState & TerminalKeyListener.META_SHIFT_LOCK) != 0)
211 canvas.drawPath(shiftCursor, cursorPaint);
212
213 if ((metaState & TerminalKeyListener.META_ALT_ON) != 0)
214 canvas.drawPath(altCursor, cursorStrokePaint);
215 else if ((metaState & TerminalKeyListener.META_ALT_LOCK) != 0)
216 canvas.drawPath(altCursor, cursorPaint);
217
218 if ((metaState & TerminalKeyListener.META_CTRL_ON) != 0)
219 canvas.drawPath(ctrlCursor, cursorStrokePaint);
220 else if ((metaState & TerminalKeyListener.META_CTRL_LOCK) != 0)
221 canvas.drawPath(ctrlCursor, cursorPaint);
222
223 // Restore previous clip region
224 canvas.restore();
225 }
226
227 // draw any highlighted area
228 if (bridge.isSelectingForCopy()) {
229 SelectionArea area = bridge.getSelectionArea();
230 canvas.save(Canvas.CLIP_SAVE_FLAG);
231 canvas.clipRect(
232 area.getLeft() * bridge.charWidth,
233 area.getTop() * bridge.charHeight,
234 (area.getRight() + 1) * bridge.charWidth,
235 (area.getBottom() + 1) * bridge.charHeight
236 );
237 canvas.drawPaint(cursorPaint);
238 canvas.restore();
239 }
240 }
241 }
242
243 public void notifyUser(String message) {
244 if (!notifications)
245 return;
246
247 if (notification != null) {
248 // Don't keep telling the user the same thing.
249 if (lastNotification != null && lastNotification.equals(message))
250 return;
251
252 notification.setText(message);
253 notification.show();
254 }
255 else {
256 notification = Toast.makeText(context, message, Toast.LENGTH_SHORT);
257 notification.show();
258 }
259
260 lastNotification = message;
261 }
262
263 /**
264 * Ask the {@link TerminalBridge} we're connected to to resize to a specific size.
265 * @param width
266 * @param height
267 */
268 public void forceSize(int width, int height) {
269 bridge.resizeComputed(width, height, getWidth(), getHeight());
270 }
271
272 /**
273 * Sets the ability for the TerminalView to display Toast notifications to the user.
274 * @param value whether to enable notifications or not
275 */
276 public void setNotifications(boolean value) {
277 notifications = value;
278 }
279
280 @Override
281 public boolean onCheckIsTextEditor() {
282 return true;
283 }
284
285 @Override
286 public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
287 outAttrs.imeOptions |=
288 EditorInfo.IME_FLAG_NO_EXTRACT_UI |
289 EditorInfo.IME_FLAG_NO_ENTER_ACTION |
290 EditorInfo.IME_ACTION_NONE;
291 outAttrs.inputType = EditorInfo.TYPE_NULL;
292 return new BaseInputConnection(this, false) {
293 @Override
294 public boolean deleteSurroundingText(int leftLength, int rightLength) {
295 if (rightLength == 0 && leftLength == 0) {
296 return this.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL));
297 }
298
299 for (int i = 0; i < leftLength; i++) {
300 this.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL));
301 }
302
303 // TODO: forward delete
304 return true;
305 }
306 };
307 }
308
309 public void propagateConsoleText(char[] rawText, int length) {
310 if (mAccessibilityActive) {
311 synchronized (mAccessibilityLock) {
312 mAccessibilityBuffer.append(rawText, 0, length);
313 }
314
315 if (mAccessibilityInitialized) {
316 if (mEventSender != null) {
317 removeCallbacks(mEventSender);
318 }
319 else {
320 mEventSender = new AccessibilityEventSender();
321 }
322
323 postDelayed(mEventSender, ACCESSIBILITY_EVENT_THRESHOLD);
324 }
325 }
326 }
327
328 private class AccessibilityEventSender implements Runnable {
329 public void run() {
330 synchronized (mAccessibilityLock) {
331 if (mCodeMatcher == null) {
332 mCodeMatcher = mControlCodes.matcher(mAccessibilityBuffer);
333 }
334 else {
335 mCodeMatcher.reset(mAccessibilityBuffer);
336 }
337
338 // Strip all control codes out.
339 mAccessibilityBuffer = new StringBuffer(mCodeMatcher.replaceAll(" "));
340 // Apply Backspaces using backspace character sequence
341 int i = mAccessibilityBuffer.indexOf(BACKSPACE_CODE);
342
343 while (i != -1) {
344 mAccessibilityBuffer = mAccessibilityBuffer.replace(i == 0 ? 0 : i - 1, i
345 + BACKSPACE_CODE.length(), "");
346 i = mAccessibilityBuffer.indexOf(BACKSPACE_CODE);
347 }
348
349 if (mAccessibilityBuffer.length() > 0) {
350 AccessibilityEvent event = AccessibilityEvent.obtain(
351 AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED);
352 event.setFromIndex(0);
353 event.setAddedCount(mAccessibilityBuffer.length());
354 event.getText().add(mAccessibilityBuffer);
355 sendAccessibilityEventUnchecked(event);
356 mAccessibilityBuffer.setLength(0);
357 }
358 }
359 }
360 }
361
362 private class AccessibilityStateTester extends AsyncTask<Void, Void, Boolean> {
363 @Override
364 protected Boolean doInBackground(Void... params) {
365 /*
366 * Presumably if the accessibility manager is not enabled, we don't
367 * need to send accessibility events.
368 */
369 final AccessibilityManager accessibility = (AccessibilityManager) context
370 .getSystemService(Context.ACCESSIBILITY_SERVICE);
371
372 if (!accessibility.isEnabled()) {
373 return false;
374 }
375
376 /*
377 * Restrict the set of intents to only accessibility services that
378 * have the category FEEDBACK_SPOKEN (aka, screen readers).
379 */
380 final Intent screenReaderIntent = new Intent(SCREENREADER_INTENT_ACTION);
381 screenReaderIntent.addCategory(SCREENREADER_INTENT_CATEGORY);
382 final ContentResolver cr = context.getContentResolver();
383 final List<ResolveInfo> screenReaders = context.getPackageManager().queryIntentServices(
384 screenReaderIntent, 0);
385 boolean foundScreenReader = false;
386 final int N = screenReaders.size();
387
388 for (int i = 0; i < N; i++) {
389 final ResolveInfo screenReader = screenReaders.get(i);
390 /*
391 * All screen readers are expected to implement a content
392 * provider that responds to:
393 * content://<nameofpackage>.providers.StatusProvider
394 */
395 final Cursor cursor = cr.query(
396 Uri.parse("content://" + screenReader.serviceInfo.packageName
397 + ".providers.StatusProvider"), null, null, null, null);
398
399 if (cursor != null && cursor.moveToFirst()) {
400 /*
401 * These content providers use a special cursor that only has
402 * one element, an integer that is 1 if the screen reader is
403 * running.
404 */
405 final int status = cursor.getInt(0);
406 cursor.close();
407
408 if (status == 1) {
409 foundScreenReader = true;
410 break;
411 }
412 }
413 }
414
415 if (foundScreenReader) {
416 mControlCodes = Pattern.compile(CONTROL_CODE_PATTERN);
417 }
418
419 return foundScreenReader;
420 }
421
422 @Override
423 protected void onPostExecute(Boolean result) {
424 mAccessibilityActive = result;
425 mAccessibilityInitialized = true;
426
427 if (result) {
428 mEventSender = new AccessibilityEventSender();
429 postDelayed(mEventSender, ACCESSIBILITY_EVENT_THRESHOLD);
430 }
431 else {
432 mAccessibilityBuffer = null;
433 }
434 }
435 }
436
437 private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
438 @Override
439 public boolean onScale(ScaleGestureDetector detector) {
440 float mScaleFactor = detector.getScaleFactor();
441
442 if (mScaleFactor > 1.1) {
443 bridge.increaseFontSize();
444 return true;
445 }
446 else if (mScaleFactor < 0.9) {
447 bridge.decreaseFontSize();
448 return true;
449 }
450
451 return (false);
452 }
453 }
454
455 }