comparison app/src/main/java/com/five_ten_sg/connectbot/TerminalView.java @ 438:d29cce60f393

migrate from Eclipse to Android Studio
author Carl Byington <carl@five-ten-sg.com>
date Thu, 03 Dec 2015 11:23:55 -0800
parents src/com/five_ten_sg/connectbot/TerminalView.java@e5f7c2584296
children 105815cce146
comparison
equal deleted inserted replaced
437:208b31032318 438:d29cce60f393
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.util.Log;
43 import android.view.KeyEvent;
44 import android.view.View;
45 import android.view.ViewGroup.LayoutParams;
46 import android.view.accessibility.AccessibilityEvent;
47 import android.view.accessibility.AccessibilityManager;
48 import android.view.inputmethod.BaseInputConnection;
49 import android.view.inputmethod.EditorInfo;
50 import android.view.inputmethod.InputConnection;
51 import android.view.ScaleGestureDetector;
52 import android.widget.Toast;
53 import de.mud.terminal.VDUBuffer;
54
55 /**
56 * User interface {@link View} for showing a TerminalBridge in an
57 * {@link Activity}. Handles drawing bitmap updates and passing keystrokes down
58 * to terminal.
59 *
60 * @author jsharkey
61 */
62 public class TerminalView extends View implements FontSizeChangedListener {
63 public final static String TAG = "ConnectBot.TerminalView";
64
65 private final Context context;
66 public final TerminalBridge bridge;
67 private final Paint paint;
68 private final Paint cursorPaint;
69 private final Paint cursorStrokePaint;
70
71 // Cursor paints to distinguish modes
72 private Path ctrlCursor, altCursor, shiftCursor;
73 private RectF tempSrc, tempDst;
74 private Matrix scaleMatrix;
75 private static final Matrix.ScaleToFit scaleType = Matrix.ScaleToFit.FILL;
76
77 private Toast notification = null;
78 private String lastNotification = null;
79 private volatile boolean notifications = true;
80
81 // Related to Accessibility Features
82 private boolean mAccessibilityInitialized = false;
83 private boolean mAccessibilityActive = true;
84 private Object[] mAccessibilityLock = new Object[0];
85 private StringBuffer mAccessibilityBuffer;
86 private Pattern mControlCodes = null;
87 private Matcher mCodeMatcher = null;
88 private AccessibilityEventSender mEventSender = null;
89
90
91 private static final String BACKSPACE_CODE = "\\x08\\x1b\\[K";
92 private static final String CONTROL_CODE_PATTERN = "\\x1b\\[K[^m]+[m|:]";
93
94 private static final int ACCESSIBILITY_EVENT_THRESHOLD = 1000;
95 private static final String SCREENREADER_INTENT_ACTION = "android.accessibilityservice.AccessibilityService";
96 private static final String SCREENREADER_INTENT_CATEGORY = "android.accessibilityservice.category.FEEDBACK_SPOKEN";
97
98 public ScaleGestureDetector mScaleDetector;
99
100 public TerminalView(Context context, TerminalBridge bridge) {
101 super(context);
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 try {
248 // ignore if don't want notifications
249 if (!notifications) return;
250 // Don't keep telling the user the same thing.
251 if (lastNotification != null && lastNotification.equals(message)) return;
252 // create or use old toast
253 if (notification == null) {
254 notification = Toast.makeText(context, message, Toast.LENGTH_SHORT);
255 }
256 else {
257 notification.setText(message);
258 }
259 notification.show();
260 lastNotification = message;
261 }
262 catch (Exception e) {
263 Log.e(TAG, "Problem while trying to notify user", e);
264 }
265 }
266
267 /**
268 * Ask the {@link TerminalBridge} we're connected to to resize to a specific size.
269 * @param width
270 * @param height
271 */
272 public void forceSize(int width, int height) {
273 bridge.resizeComputed(width, height, getWidth(), getHeight());
274 }
275
276 /**
277 * Sets the ability for the TerminalView to display Toast notifications to the user.
278 * @param value whether to enable notifications or not
279 */
280 public void setNotifications(boolean value) {
281 notifications = value;
282 }
283
284 @Override
285 public boolean onCheckIsTextEditor() {
286 return true;
287 }
288
289 @Override
290 public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
291 outAttrs.imeOptions |=
292 EditorInfo.IME_FLAG_NO_EXTRACT_UI |
293 EditorInfo.IME_FLAG_NO_ENTER_ACTION |
294 EditorInfo.IME_ACTION_NONE;
295 outAttrs.inputType = EditorInfo.TYPE_NULL;
296 return new BaseInputConnection(this, false) {
297 @Override
298 public boolean deleteSurroundingText(int leftLength, int rightLength) {
299 if (rightLength == 0 && leftLength == 0) {
300 return this.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL));
301 }
302
303 for (int i = 0; i < leftLength; i++) {
304 this.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL));
305 }
306
307 // TODO: forward delete
308 return true;
309 }
310 };
311 }
312
313 public void propagateConsoleText(char[] rawText, int length) {
314 if (mAccessibilityActive) {
315 synchronized (mAccessibilityLock) {
316 mAccessibilityBuffer.append(rawText, 0, length);
317 }
318
319 if (mAccessibilityInitialized) {
320 if (mEventSender != null) {
321 removeCallbacks(mEventSender);
322 }
323 else {
324 mEventSender = new AccessibilityEventSender();
325 }
326
327 postDelayed(mEventSender, ACCESSIBILITY_EVENT_THRESHOLD);
328 }
329 }
330 }
331
332 private class AccessibilityEventSender implements Runnable {
333 public void run() {
334 synchronized (mAccessibilityLock) {
335 if (mCodeMatcher == null) {
336 mCodeMatcher = mControlCodes.matcher(mAccessibilityBuffer);
337 }
338 else {
339 mCodeMatcher.reset(mAccessibilityBuffer);
340 }
341
342 // Strip all control codes out.
343 mAccessibilityBuffer = new StringBuffer(mCodeMatcher.replaceAll(" "));
344 // Apply Backspaces using backspace character sequence
345 int i = mAccessibilityBuffer.indexOf(BACKSPACE_CODE);
346
347 while (i != -1) {
348 mAccessibilityBuffer = mAccessibilityBuffer.replace(i == 0 ? 0 : i - 1, i
349 + BACKSPACE_CODE.length(), "");
350 i = mAccessibilityBuffer.indexOf(BACKSPACE_CODE);
351 }
352
353 if (mAccessibilityBuffer.length() > 0) {
354 AccessibilityEvent event = AccessibilityEvent.obtain(
355 AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED);
356 event.setFromIndex(0);
357 event.setAddedCount(mAccessibilityBuffer.length());
358 event.getText().add(mAccessibilityBuffer);
359 sendAccessibilityEventUnchecked(event);
360 mAccessibilityBuffer.setLength(0);
361 }
362 }
363 }
364 }
365
366 private class AccessibilityStateTester extends AsyncTask<Void, Void, Boolean> {
367 @Override
368 protected Boolean doInBackground(Void... params) {
369 /*
370 * Presumably if the accessibility manager is not enabled, we don't
371 * need to send accessibility events.
372 */
373 final AccessibilityManager accessibility = (AccessibilityManager) context
374 .getSystemService(Context.ACCESSIBILITY_SERVICE);
375
376 if (!accessibility.isEnabled()) {
377 return false;
378 }
379
380 /*
381 * Restrict the set of intents to only accessibility services that
382 * have the category FEEDBACK_SPOKEN (aka, screen readers).
383 */
384 final Intent screenReaderIntent = new Intent(SCREENREADER_INTENT_ACTION);
385 screenReaderIntent.addCategory(SCREENREADER_INTENT_CATEGORY);
386 final ContentResolver cr = context.getContentResolver();
387 final List<ResolveInfo> screenReaders = context.getPackageManager().queryIntentServices(
388 screenReaderIntent, 0);
389 boolean foundScreenReader = false;
390 final int N = screenReaders.size();
391
392 for (int i = 0; i < N; i++) {
393 final ResolveInfo screenReader = screenReaders.get(i);
394 /*
395 * All screen readers are expected to implement a content
396 * provider that responds to:
397 * content://<nameofpackage>.providers.StatusProvider
398 */
399 final Cursor cursor = cr.query(
400 Uri.parse("content://" + screenReader.serviceInfo.packageName
401 + ".providers.StatusProvider"), null, null, null, null);
402
403 if (cursor != null && cursor.moveToFirst()) {
404 /*
405 * These content providers use a special cursor that only has
406 * one element, an integer that is 1 if the screen reader is
407 * running.
408 */
409 final int status = cursor.getInt(0);
410 cursor.close();
411
412 if (status == 1) {
413 foundScreenReader = true;
414 break;
415 }
416 }
417 }
418
419 if (foundScreenReader) {
420 mControlCodes = Pattern.compile(CONTROL_CODE_PATTERN);
421 }
422
423 return foundScreenReader;
424 }
425
426 @Override
427 protected void onPostExecute(Boolean result) {
428 mAccessibilityActive = result;
429 mAccessibilityInitialized = true;
430
431 if (result) {
432 mEventSender = new AccessibilityEventSender();
433 postDelayed(mEventSender, ACCESSIBILITY_EVENT_THRESHOLD);
434 }
435 else {
436 mAccessibilityBuffer = null;
437 }
438 }
439 }
440
441 private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
442 @Override
443 public boolean onScale(ScaleGestureDetector detector) {
444 float mScaleFactor = detector.getScaleFactor();
445
446 if (mScaleFactor > 1.1) {
447 bridge.increaseFontSize();
448 return true;
449 }
450 else if (mScaleFactor < 0.9) {
451 bridge.decreaseFontSize();
452 return true;
453 }
454
455 return (false);
456 }
457 }
458
459 }