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