FreeRDP
Loading...
Searching...
No Matches
SessionView.java
1/*
2 Android Session view
3
4 Copyright 2013 Thincast Technologies GmbH, Author: Martin Fleisz
5
6 This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
7 If a copy of the MPL was not distributed with this file, You can obtain one at
8 http://mozilla.org/MPL/2.0/.
9*/
10
11package com.freerdp.freerdpcore.presentation;
12
13import android.content.Context;
14import android.graphics.Bitmap;
15import android.graphics.Canvas;
16import android.graphics.Color;
17import android.graphics.Matrix;
18import android.graphics.Rect;
19import android.graphics.RectF;
20import android.graphics.drawable.BitmapDrawable;
21import android.text.InputType;
22import android.util.AttributeSet;
23import android.util.Log;
24import android.view.InputDevice;
25import android.view.MotionEvent;
26import android.view.PointerIcon;
27import android.view.ScaleGestureDetector;
28import android.view.View;
29import android.view.ViewConfiguration;
30import android.view.inputmethod.BaseInputConnection;
31import android.view.inputmethod.EditorInfo;
32import android.view.inputmethod.InputConnection;
33
34import androidx.annotation.NonNull;
35
36import com.freerdp.freerdpcore.application.SessionState;
37import com.freerdp.freerdpcore.utils.DoubleGestureDetector;
38import com.freerdp.freerdpcore.utils.GestureDetector;
39
40import java.util.Stack;
41
42public class SessionView extends View
43{
44 public static final float MAX_SCALE_FACTOR = 3.0f;
45 public static final float MIN_SCALE_FACTOR = 0.75f;
46 private static final String TAG = "SessionView";
47 private static final float SCALE_FACTOR_DELTA = 0.0001f;
48 private static final float TOUCH_SCROLL_DELTA = 10.0f;
49 private int width;
50 private int height;
51 private BitmapDrawable surface;
52 private Stack<Rect> invalidRegions;
53 private int touchPointerPaddingWidth = 0;
54 private int touchPointerPaddingHeight = 0;
55 private SessionViewListener sessionViewListener = null;
56 private OnZoomChangedListener zoomChangedListener = null;
57 private boolean railMode = false;
58 // helpers for scaling gesture handling
59 private float scaleFactor = 1.0f;
60 private Matrix scaleMatrix;
61 private Matrix invScaleMatrix;
62 private RectF invalidRegionF;
63 private GestureDetector gestureDetector;
64 private SessionState currentSession;
65
66 // Pixels of touchpad travel per wheel notch, and leftover between events.
67 private float notchPxX = 1.0f;
68 private float notchPxY = 1.0f;
69 private float touchpadScrollX = 0;
70 private float touchpadScrollY = 0;
71
72 private int[] cursorPixels = null;
73 private int cursorWidth = 0;
74 private int cursorHeight = 0;
75 private int cursorHotX = 0;
76 private int cursorHotY = 0;
77
78 // private static final String TAG = "FreeRDP.SessionView";
79 private DoubleGestureDetector doubleGestureDetector;
80 public SessionView(Context context)
81 {
82 super(context);
83 initSessionView(context);
84 }
85
86 public SessionView(Context context, AttributeSet attrs)
87 {
88 super(context, attrs);
89 initSessionView(context);
90 }
91
92 public SessionView(Context context, AttributeSet attrs, int defStyle)
93 {
94 super(context, attrs, defStyle);
95 initSessionView(context);
96 }
97
98 private void initSessionView(Context context)
99 {
100 // Ensure the view is focusable so the soft keyboard can attach to it (required for API 30+)
101 setFocusable(true);
102 setFocusableInTouchMode(true);
103
104 invalidRegions = new Stack<>();
105 gestureDetector = new GestureDetector(context, new SessionGestureListener(), null, true);
106 ViewConfiguration vc = ViewConfiguration.get(context);
107 notchPxX = Math.max(1.0f, vc.getScaledHorizontalScrollFactor());
108 notchPxY = Math.max(1.0f, vc.getScaledVerticalScrollFactor());
109 doubleGestureDetector =
110 new DoubleGestureDetector(context, null, new SessionDoubleGestureListener());
111
112 scaleFactor = 1.0f;
113 scaleMatrix = new Matrix();
114 invScaleMatrix = new Matrix();
115 invalidRegionF = new RectF();
116 }
117
118 /* External Mouse Hover */
119 @Override public boolean onHoverEvent(MotionEvent event)
120 {
121 if (event.getAction() == MotionEvent.ACTION_HOVER_MOVE)
122 {
123 // Handle hover move event
124 float x = event.getX();
125 float y = event.getY();
126 // Perform actions based on the hover position (x, y)
127 MotionEvent mappedEvent = mapTouchEvent(event);
128 sessionViewListener.onSessionViewMouseMove((int)mappedEvent.getX(),
129 (int)mappedEvent.getY());
130 mappedEvent.recycle();
131 }
132 // Return true to indicate that you've handled the event
133 return true;
134 }
135
136 public void setScaleGestureDetector(ScaleGestureDetector scaleGestureDetector)
137 {
138 doubleGestureDetector.setScaleGestureDetector(scaleGestureDetector);
139 }
140
141 public void setSessionViewListener(SessionViewListener sessionViewListener)
142 {
143 this.sessionViewListener = sessionViewListener;
144 }
145
146 public void addInvalidRegion(Rect invalidRegion)
147 {
148 // correctly transform invalid region depending on current scaling
149 invalidRegionF.set(invalidRegion);
150 scaleMatrix.mapRect(invalidRegionF);
151 invalidRegionF.roundOut(invalidRegion);
152
153 invalidRegions.add(invalidRegion);
154 }
155
156 public void invalidateRegion()
157 {
158 invalidate(invalidRegions.pop());
159 }
160
161 public void onSurfaceChange(SessionState session)
162 {
163 surface = session.getSurface();
164 Bitmap bitmap = surface.getBitmap();
165 width = bitmap.getWidth();
166 height = bitmap.getHeight();
167 surface.setBounds(0, 0, width, height);
168
169 setMinimumWidth(width);
170 setMinimumHeight(height);
171
172 requestLayout();
173 currentSession = session;
174 }
175
176 public float getZoom()
177 {
178 return scaleFactor;
179 }
180
181 public void setRailMode(boolean rail)
182 {
183 if (railMode == rail)
184 return;
185 railMode = rail;
186 invalidate();
187 }
188
189 public interface OnZoomChangedListener
190 {
191 void onZoomChanged(float zoom);
192 }
193
194 public void setOnZoomChangedListener(OnZoomChangedListener l)
195 {
196 zoomChangedListener = l;
197 }
198
199 public void setZoom(float factor)
200 {
201 scaleFactor = factor;
202 scaleMatrix.setScale(scaleFactor, scaleFactor);
203 invScaleMatrix.setScale(1.0f / scaleFactor, 1.0f / scaleFactor);
204
205 if (cursorPixels != null)
206 applyScaledCursor();
207
208 if (zoomChangedListener != null)
209 zoomChangedListener.onZoomChanged(scaleFactor);
210
211 requestLayout();
212 }
213
214 public boolean isAtMaxZoom()
215 {
216 return (scaleFactor > (MAX_SCALE_FACTOR - SCALE_FACTOR_DELTA));
217 }
218
219 public boolean isAtMinZoom()
220 {
221 return (scaleFactor < (MIN_SCALE_FACTOR + SCALE_FACTOR_DELTA));
222 }
223
224 public boolean zoomIn(float factor)
225 {
226 boolean res = true;
227 scaleFactor += factor;
228 if (scaleFactor > (MAX_SCALE_FACTOR - SCALE_FACTOR_DELTA))
229 {
230 scaleFactor = MAX_SCALE_FACTOR;
231 res = false;
232 }
233 setZoom(scaleFactor);
234 return res;
235 }
236
237 public boolean zoomOut(float factor)
238 {
239 boolean res = true;
240 scaleFactor -= factor;
241 if (scaleFactor < (MIN_SCALE_FACTOR + SCALE_FACTOR_DELTA))
242 {
243 scaleFactor = MIN_SCALE_FACTOR;
244 res = false;
245 }
246 setZoom(scaleFactor);
247 return res;
248 }
249
250 public void setTouchPointerPadding(int width, int height)
251 {
252 touchPointerPaddingWidth = width;
253 touchPointerPaddingHeight = height;
254 requestLayout();
255 }
256
257 public int getTouchPointerPaddingWidth()
258 {
259 return touchPointerPaddingWidth;
260 }
261
262 public int getTouchPointerPaddingHeight()
263 {
264 return touchPointerPaddingHeight;
265 }
266
267 @Override public void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
268 {
269 Log.v(TAG, width + "x" + height);
270 this.setMeasuredDimension((int)(width * scaleFactor) + touchPointerPaddingWidth,
271 (int)(height * scaleFactor) + touchPointerPaddingHeight);
272 }
273
274 @Override public void onDraw(@NonNull Canvas canvas)
275 {
276 super.onDraw(canvas);
277
278 canvas.save();
279 canvas.concat(scaleMatrix);
280 canvas.drawColor(Color.BLACK);
281 if (!railMode && surface != null)
282 {
283 surface.draw(canvas);
284 }
285 canvas.restore();
286 }
287
288 // perform mapping on the touch event's coordinates according to the current scaling
289 private MotionEvent mapTouchEvent(MotionEvent event)
290 {
291 MotionEvent mappedEvent = MotionEvent.obtain(event);
292 float[] coordinates = { mappedEvent.getX(), mappedEvent.getY() };
293 invScaleMatrix.mapPoints(coordinates);
294 mappedEvent.setLocation(coordinates[0], coordinates[1]);
295 return mappedEvent;
296 }
297
298 // perform mapping on the double touch event's coordinates according to the current scaling
299 private MotionEvent mapDoubleTouchEvent(MotionEvent event)
300 {
301 MotionEvent mappedEvent = MotionEvent.obtain(event);
302 float[] coordinates = { (mappedEvent.getX(0) + mappedEvent.getX(1)) / 2,
303 (mappedEvent.getY(0) + mappedEvent.getY(1)) / 2 };
304 invScaleMatrix.mapPoints(coordinates);
305 mappedEvent.setLocation(coordinates[0], coordinates[1]);
306 return mappedEvent;
307 }
308
309 @Override public boolean onTouchEvent(MotionEvent event)
310 {
311 // Physical mouse events: bypass gesture detector entirely.
312 // Buttons are handled in onGenericMotionEvent; hover moves in onHoverEvent.
313 // Only ACTION_MOVE with a button held (drag) needs handling here.
314 if (event.isFromSource(InputDevice.SOURCE_MOUSE))
315 {
316 int action = event.getActionMasked();
317 if (event.getClassification() == MotionEvent.CLASSIFICATION_TWO_FINGER_SWIPE)
318 {
319 handleTouchpadScroll(event);
320 return true;
321 }
322 if (action == MotionEvent.ACTION_MOVE && event.getButtonState() != 0)
323 {
324 MotionEvent mapped = mapTouchEvent(event);
325 sessionViewListener.onSessionViewMouseMove((int)mapped.getX(), (int)mapped.getY());
326 mapped.recycle();
327 return true;
328 }
329 return true;
330 }
331
332 boolean res = gestureDetector.onTouchEvent(event);
333 res |= doubleGestureDetector.onTouchEvent(event);
334 return res;
335 }
336
337 // Handle physical touchpad two-finger scroll gesture.
338 private void handleTouchpadScroll(MotionEvent event)
339 {
340 if (event.getActionMasked() != MotionEvent.ACTION_MOVE)
341 {
342 touchpadScrollX = 0;
343 touchpadScrollY = 0;
344 return;
345 }
346
347 touchpadScrollY += gestureDistance(event, MotionEvent.AXIS_GESTURE_SCROLL_Y_DISTANCE);
348 while (Math.abs(touchpadScrollY) >= notchPxY)
349 {
350 final boolean down = touchpadScrollY > 0;
351 touchpadScrollY += down ? -notchPxY : notchPxY;
352 sessionViewListener.onSessionViewScroll(down);
353 }
354
355 touchpadScrollX += gestureDistance(event, MotionEvent.AXIS_GESTURE_SCROLL_X_DISTANCE);
356 while (Math.abs(touchpadScrollX) >= notchPxX)
357 {
358 final boolean right = touchpadScrollX > 0;
359 touchpadScrollX += right ? -notchPxX : notchPxX;
360 sessionViewListener.onSessionViewHScroll(right);
361 }
362 }
363
364 // Sum batched historical and current sample deltas for a gesture axis.
365 private static float gestureDistance(MotionEvent event, int axis)
366 {
367 float distance = 0;
368 for (int i = 0; i < event.getHistorySize(); i++)
369 distance += event.getHistoricalAxisValue(axis, 0, i);
370 return distance + event.getAxisValue(axis);
371 }
372
373 // Handle all physical mouse buttons here; finger taps come via onSingleTapUp.
374 @Override public boolean onGenericMotionEvent(MotionEvent event)
375 {
376 final boolean isPointer = event.isFromSource(InputDevice.SOURCE_CLASS_POINTER);
377 if (!isPointer)
378 return false;
379
380 final boolean isMouse = event.isFromSource(InputDevice.SOURCE_MOUSE);
381 int action = event.getActionMasked();
382
383 if (isMouse && (action == MotionEvent.ACTION_BUTTON_PRESS ||
384 action == MotionEvent.ACTION_BUTTON_RELEASE))
385 {
386 boolean down = action == MotionEvent.ACTION_BUTTON_PRESS;
387 MotionEvent mapped = mapTouchEvent(event);
388 int x = (int)mapped.getX();
389 int y = (int)mapped.getY();
390 mapped.recycle();
391
392 switch (event.getActionButton())
393 {
394 case MotionEvent.BUTTON_PRIMARY:
395 if (down)
396 sessionViewListener.onSessionViewBeginTouch();
397 sessionViewListener.onSessionViewLeftTouch(x, y, down);
398 if (!down)
399 sessionViewListener.onSessionViewEndTouch();
400 return true;
401 case MotionEvent.BUTTON_SECONDARY:
402 if (down)
403 sessionViewListener.onSessionViewBeginTouch();
404 sessionViewListener.onSessionViewRightTouch(x, y, down);
405 return true;
406 case MotionEvent.BUTTON_TERTIARY:
407 sessionViewListener.onSessionViewMiddleTouch(x, y, down);
408 return true;
409 default:
410 return true; // consume unknown buttons silently
411 }
412 }
413
414 if (action == MotionEvent.ACTION_SCROLL)
415 {
416 float vScroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL);
417 float hScroll = event.getAxisValue(MotionEvent.AXIS_HSCROLL);
418 if (vScroll != 0)
419 sessionViewListener.onSessionViewScroll(vScroll > 0);
420 if (hScroll != 0)
421 sessionViewListener.onSessionViewHScroll(hScroll > 0);
422 return true;
423 }
424
425 return false;
426 }
427
428 public interface SessionViewListener
429 {
430 void onSessionViewBeginTouch();
431
432 void onSessionViewEndTouch();
433
434 void onSessionViewLeftTouch(int x, int y, boolean down);
435
436 void onSessionViewMiddleTouch(int x, int y, boolean down);
437
438 void onSessionViewRightTouch(int x, int y, boolean down);
439
440 void onSessionViewMove(int x, int y);
441
442 void onSessionViewMouseMove(int x, int y);
443
444 void onSessionViewScroll(boolean down);
445
446 void onSessionViewHScroll(boolean right);
447 }
448
449 public void setRemoteCursor(int[] pixels, int width, int height, int hotX, int hotY)
450 {
451 if (pixels == null || width == 0 || height == 0)
452 {
453 cursorPixels = null;
454 setPointerIcon(PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_NULL));
455 return;
456 }
457 cursorPixels = pixels;
458 cursorWidth = width;
459 cursorHeight = height;
460 cursorHotX = hotX;
461 cursorHotY = hotY;
462 applyScaledCursor();
463 }
464
465 private void applyScaledCursor()
466 {
467 int scaledWidth = Math.max(1, (int)(cursorWidth * scaleFactor));
468 int scaledHeight = Math.max(1, (int)(cursorHeight * scaleFactor));
469 Bitmap bm =
470 Bitmap.createBitmap(cursorPixels, cursorWidth, cursorHeight, Bitmap.Config.ARGB_8888);
471 Bitmap scaled = Bitmap.createScaledBitmap(bm, scaledWidth, scaledHeight, true);
472 PointerIcon icon =
473 PointerIcon.create(scaled, cursorHotX * scaleFactor, cursorHotY * scaleFactor);
474 setPointerIcon(icon);
475 }
476
477 public void setDefaultCursor()
478 {
479 setPointerIcon(PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_ARROW));
480 }
481
482 private class SessionGestureListener extends GestureDetector.SimpleOnGestureListener
483 {
484 boolean longPressInProgress = false;
485
486 public boolean onDown(MotionEvent e)
487 {
488 return true;
489 }
490
491 public boolean onUp(MotionEvent e)
492 {
493 sessionViewListener.onSessionViewEndTouch();
494 return true;
495 }
496
497 public void onLongPress(MotionEvent e)
498 {
499 MotionEvent mappedEvent = mapTouchEvent(e);
500 sessionViewListener.onSessionViewBeginTouch();
501 sessionViewListener.onSessionViewLeftTouch((int)mappedEvent.getX(),
502 (int)mappedEvent.getY(), true);
503 longPressInProgress = true;
504 }
505
506 public void onLongPressUp(MotionEvent e)
507 {
508 MotionEvent mappedEvent = mapTouchEvent(e);
509 sessionViewListener.onSessionViewLeftTouch((int)mappedEvent.getX(),
510 (int)mappedEvent.getY(), false);
511 longPressInProgress = false;
512 sessionViewListener.onSessionViewEndTouch();
513 }
514
515 public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY)
516 {
517 if (longPressInProgress)
518 {
519 MotionEvent mappedEvent = mapTouchEvent(e2);
520 sessionViewListener.onSessionViewMove((int)mappedEvent.getX(),
521 (int)mappedEvent.getY());
522 return true;
523 }
524
525 return false;
526 }
527
528 public boolean onDoubleTap(MotionEvent e)
529 {
530 // send 2nd click for double click
531 MotionEvent mappedEvent = mapTouchEvent(e);
532 sessionViewListener.onSessionViewLeftTouch((int)mappedEvent.getX(),
533 (int)mappedEvent.getY(), true);
534 sessionViewListener.onSessionViewLeftTouch((int)mappedEvent.getX(),
535 (int)mappedEvent.getY(), false);
536 return true;
537 }
538
539 public boolean onSingleTapUp(MotionEvent e)
540 {
541 // Physical mouse buttons are handled via ACTION_BUTTON_PRESS in onGenericMotionEvent.
542 // If buttonState is non-zero this event came from a physical mouse button
543 if (e.getButtonState() != 0)
544 return false;
545
546 // Finger touch -> left click
547 MotionEvent mappedEvent = mapTouchEvent(e);
548 sessionViewListener.onSessionViewBeginTouch();
549 sessionViewListener.onSessionViewLeftTouch((int)mappedEvent.getX(),
550 (int)mappedEvent.getY(), true);
551 sessionViewListener.onSessionViewLeftTouch((int)mappedEvent.getX(),
552 (int)mappedEvent.getY(), false);
553 sessionViewListener.onSessionViewEndTouch();
554 return true;
555 }
556 }
557
558 private class SessionDoubleGestureListener
559 implements DoubleGestureDetector.OnDoubleGestureListener
560 {
561 private MotionEvent prevEvent = null;
562
563 public boolean onDoubleTouchDown(MotionEvent e)
564 {
565 sessionViewListener.onSessionViewBeginTouch();
566 prevEvent = MotionEvent.obtain(e);
567 return true;
568 }
569
570 public boolean onDoubleTouchUp(MotionEvent e)
571 {
572 if (prevEvent != null)
573 {
574 prevEvent.recycle();
575 prevEvent = null;
576 }
577 sessionViewListener.onSessionViewEndTouch();
578 return true;
579 }
580
581 public boolean onDoubleTouchScroll(MotionEvent e1, MotionEvent e2)
582 {
583 // calc if user scrolled up or down (or if any scrolling happened at all)
584 float deltaY = e2.getY() - prevEvent.getY();
585 if (deltaY > TOUCH_SCROLL_DELTA)
586 {
587 sessionViewListener.onSessionViewScroll(true);
588 prevEvent.recycle();
589 prevEvent = MotionEvent.obtain(e2);
590 }
591 else if (deltaY < -TOUCH_SCROLL_DELTA)
592 {
593 sessionViewListener.onSessionViewScroll(false);
594 prevEvent.recycle();
595 prevEvent = MotionEvent.obtain(e2);
596 }
597 return true;
598 }
599
600 public boolean onDoubleTouchSingleTap(MotionEvent e)
601 {
602 // send single click
603 MotionEvent mappedEvent = mapDoubleTouchEvent(e);
604 sessionViewListener.onSessionViewRightTouch((int)mappedEvent.getX(),
605 (int)mappedEvent.getY(), true);
606 sessionViewListener.onSessionViewRightTouch((int)mappedEvent.getX(),
607 (int)mappedEvent.getY(), false);
608 return true;
609 }
610 }
611
612 @Override public InputConnection onCreateInputConnection(EditorInfo outAttrs)
613 {
614 outAttrs.actionLabel = null;
615 outAttrs.inputType = InputType.TYPE_NULL;
616 outAttrs.imeOptions = EditorInfo.IME_ACTION_NONE | EditorInfo.IME_FLAG_NO_EXTRACT_UI |
617 EditorInfo.IME_FLAG_NO_FULLSCREEN;
618 return new BaseInputConnection(this, false);
619 }
620}