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