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