FreeRDP
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 
11 package com.freerdp.freerdpcore.presentation;
12 
13 import android.content.Context;
14 import android.graphics.Bitmap;
15 import android.graphics.Canvas;
16 import android.graphics.Color;
17 import android.graphics.Matrix;
18 import android.graphics.Rect;
19 import android.graphics.RectF;
20 import android.graphics.drawable.BitmapDrawable;
21 import android.text.InputType;
22 import android.util.AttributeSet;
23 import android.util.Log;
24 import android.view.KeyEvent;
25 import android.view.MotionEvent;
26 import android.view.ScaleGestureDetector;
27 import android.view.View;
28 import android.view.inputmethod.EditorInfo;
29 import android.view.inputmethod.InputConnection;
30 
31 import com.freerdp.freerdpcore.application.SessionState;
32 import com.freerdp.freerdpcore.services.LibFreeRDP;
33 import com.freerdp.freerdpcore.utils.DoubleGestureDetector;
34 import com.freerdp.freerdpcore.utils.GestureDetector;
35 import com.freerdp.freerdpcore.utils.Mouse;
36 
37 import java.util.Stack;
38 
39 public class SessionView extends View
40 {
41  public static final float MAX_SCALE_FACTOR = 3.0f;
42  public static final float MIN_SCALE_FACTOR = 1.0f;
43  private static final String TAG = "SessionView";
44  private static final float SCALE_FACTOR_DELTA = 0.0001f;
45  private static final float TOUCH_SCROLL_DELTA = 10.0f;
46  private int width;
47  private int height;
48  private BitmapDrawable surface;
49  private Stack<Rect> invalidRegions;
50  private int touchPointerPaddingWidth = 0;
51  private int touchPointerPaddingHeight = 0;
52  private SessionViewListener sessionViewListener = null;
53  // helpers for scaling gesture handling
54  private float scaleFactor = 1.0f;
55  private Matrix scaleMatrix;
56  private Matrix invScaleMatrix;
57  private RectF invalidRegionF;
58  private GestureDetector gestureDetector;
59  private SessionState currentSession;
60 
61  // private static final String TAG = "FreeRDP.SessionView";
62  private DoubleGestureDetector doubleGestureDetector;
63  public SessionView(Context context)
64  {
65  super(context);
66  initSessionView(context);
67  }
68 
69  public SessionView(Context context, AttributeSet attrs)
70  {
71  super(context, attrs);
72  initSessionView(context);
73  }
74 
75  public SessionView(Context context, AttributeSet attrs, int defStyle)
76  {
77  super(context, attrs, defStyle);
78  initSessionView(context);
79  }
80 
81  private void initSessionView(Context context)
82  {
83  invalidRegions = new Stack<>();
84  gestureDetector = new GestureDetector(context, new SessionGestureListener(), null, true);
85  doubleGestureDetector =
86  new DoubleGestureDetector(context, null, new SessionDoubleGestureListener());
87 
88  scaleFactor = 1.0f;
89  scaleMatrix = new Matrix();
90  invScaleMatrix = new Matrix();
91  invalidRegionF = new RectF();
92 
93  setSystemUiVisibility(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
94  View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
95  }
96 
97  /* External Mouse Hover */
98  @Override public boolean onHoverEvent(MotionEvent event)
99  {
100  if (event.getAction() == MotionEvent.ACTION_HOVER_MOVE)
101  {
102  // Handle hover move event
103  float x = event.getX();
104  float y = event.getY();
105  // Perform actions based on the hover position (x, y)
106  MotionEvent mappedEvent = mapTouchEvent(event);
107  LibFreeRDP.sendCursorEvent(currentSession.getInstance(), (int)mappedEvent.getX(),
108  (int)mappedEvent.getY(), Mouse.getMoveEvent());
109  }
110  // Return true to indicate that you've handled the event
111  return true;
112  }
113 
114  public void setScaleGestureDetector(ScaleGestureDetector scaleGestureDetector)
115  {
116  doubleGestureDetector.setScaleGestureDetector(scaleGestureDetector);
117  }
118 
119  public void setSessionViewListener(SessionViewListener sessionViewListener)
120  {
121  this.sessionViewListener = sessionViewListener;
122  }
123 
124  public void addInvalidRegion(Rect invalidRegion)
125  {
126  // correctly transform invalid region depending on current scaling
127  invalidRegionF.set(invalidRegion);
128  scaleMatrix.mapRect(invalidRegionF);
129  invalidRegionF.roundOut(invalidRegion);
130 
131  invalidRegions.add(invalidRegion);
132  }
133 
134  public void invalidateRegion()
135  {
136  invalidate(invalidRegions.pop());
137  }
138 
139  public void onSurfaceChange(SessionState session)
140  {
141  surface = session.getSurface();
142  Bitmap bitmap = surface.getBitmap();
143  width = bitmap.getWidth();
144  height = bitmap.getHeight();
145  surface.setBounds(0, 0, width, height);
146 
147  setMinimumWidth(width);
148  setMinimumHeight(height);
149 
150  requestLayout();
151  currentSession = session;
152  }
153 
154  public float getZoom()
155  {
156  return scaleFactor;
157  }
158 
159  public void setZoom(float factor)
160  {
161  // calc scale matrix and inverse scale matrix (to correctly transform the view and moues
162  // coordinates)
163  scaleFactor = factor;
164  scaleMatrix.setScale(scaleFactor, scaleFactor);
165  invScaleMatrix.setScale(1.0f / scaleFactor, 1.0f / scaleFactor);
166 
167  // update layout
168  requestLayout();
169  }
170 
171  public boolean isAtMaxZoom()
172  {
173  return (scaleFactor > (MAX_SCALE_FACTOR - SCALE_FACTOR_DELTA));
174  }
175 
176  public boolean isAtMinZoom()
177  {
178  return (scaleFactor < (MIN_SCALE_FACTOR + SCALE_FACTOR_DELTA));
179  }
180 
181  public boolean zoomIn(float factor)
182  {
183  boolean res = true;
184  scaleFactor += factor;
185  if (scaleFactor > (MAX_SCALE_FACTOR - SCALE_FACTOR_DELTA))
186  {
187  scaleFactor = MAX_SCALE_FACTOR;
188  res = false;
189  }
190  setZoom(scaleFactor);
191  return res;
192  }
193 
194  public boolean zoomOut(float factor)
195  {
196  boolean res = true;
197  scaleFactor -= factor;
198  if (scaleFactor < (MIN_SCALE_FACTOR + SCALE_FACTOR_DELTA))
199  {
200  scaleFactor = MIN_SCALE_FACTOR;
201  res = false;
202  }
203  setZoom(scaleFactor);
204  return res;
205  }
206 
207  public void setTouchPointerPadding(int width, int height)
208  {
209  touchPointerPaddingWidth = width;
210  touchPointerPaddingHeight = height;
211  requestLayout();
212  }
213 
214  public int getTouchPointerPaddingWidth()
215  {
216  return touchPointerPaddingWidth;
217  }
218 
219  public int getTouchPointerPaddingHeight()
220  {
221  return touchPointerPaddingHeight;
222  }
223 
224  @Override public void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
225  {
226  Log.v(TAG, width + "x" + height);
227  this.setMeasuredDimension((int)(width * scaleFactor) + touchPointerPaddingWidth,
228  (int)(height * scaleFactor) + touchPointerPaddingHeight);
229  }
230 
231  @Override public void onDraw(Canvas canvas)
232  {
233  super.onDraw(canvas);
234 
235  canvas.save();
236  canvas.concat(scaleMatrix);
237  canvas.drawColor(Color.BLACK);
238  surface.draw(canvas);
239  canvas.restore();
240  }
241 
242  // dirty hack: we call back to our activity and call onBackPressed as this doesn't reach us when
243  // the soft keyboard is shown ...
244  @Override public boolean dispatchKeyEventPreIme(KeyEvent event)
245  {
246  if (event.getKeyCode() == KeyEvent.KEYCODE_BACK &&
247  event.getAction() == KeyEvent.ACTION_DOWN)
248  ((SessionActivity)this.getContext()).onBackPressed();
249  return super.dispatchKeyEventPreIme(event);
250  }
251 
252  // perform mapping on the touch event's coordinates according to the current scaling
253  private MotionEvent mapTouchEvent(MotionEvent event)
254  {
255  MotionEvent mappedEvent = MotionEvent.obtain(event);
256  float[] coordinates = { mappedEvent.getX(), mappedEvent.getY() };
257  invScaleMatrix.mapPoints(coordinates);
258  mappedEvent.setLocation(coordinates[0], coordinates[1]);
259  return mappedEvent;
260  }
261 
262  // perform mapping on the double touch event's coordinates according to the current scaling
263  private MotionEvent mapDoubleTouchEvent(MotionEvent event)
264  {
265  MotionEvent mappedEvent = MotionEvent.obtain(event);
266  float[] coordinates = { (mappedEvent.getX(0) + mappedEvent.getX(1)) / 2,
267  (mappedEvent.getY(0) + mappedEvent.getY(1)) / 2 };
268  invScaleMatrix.mapPoints(coordinates);
269  mappedEvent.setLocation(coordinates[0], coordinates[1]);
270  return mappedEvent;
271  }
272 
273  @Override public boolean onTouchEvent(MotionEvent event)
274  {
275  boolean res = gestureDetector.onTouchEvent(event);
276  res |= doubleGestureDetector.onTouchEvent(event);
277  return res;
278  }
279 
280  public interface SessionViewListener {
281  abstract void onSessionViewBeginTouch();
282 
283  abstract void onSessionViewEndTouch();
284 
285  abstract void onSessionViewLeftTouch(int x, int y, boolean down);
286 
287  abstract void onSessionViewRightTouch(int x, int y, boolean down);
288 
289  abstract void onSessionViewMove(int x, int y);
290 
291  abstract void onSessionViewScroll(boolean down);
292  }
293 
294  private class SessionGestureListener extends GestureDetector.SimpleOnGestureListener
295  {
296  boolean longPressInProgress = false;
297 
298  public boolean onDown(MotionEvent e)
299  {
300  return true;
301  }
302 
303  public boolean onUp(MotionEvent e)
304  {
305  sessionViewListener.onSessionViewEndTouch();
306  return true;
307  }
308 
309  public void onLongPress(MotionEvent e)
310  {
311  MotionEvent mappedEvent = mapTouchEvent(e);
312  sessionViewListener.onSessionViewBeginTouch();
313  sessionViewListener.onSessionViewLeftTouch((int)mappedEvent.getX(),
314  (int)mappedEvent.getY(), true);
315  longPressInProgress = true;
316  }
317 
318  public void onLongPressUp(MotionEvent e)
319  {
320  MotionEvent mappedEvent = mapTouchEvent(e);
321  sessionViewListener.onSessionViewLeftTouch((int)mappedEvent.getX(),
322  (int)mappedEvent.getY(), false);
323  longPressInProgress = false;
324  sessionViewListener.onSessionViewEndTouch();
325  }
326 
327  public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY)
328  {
329  if (longPressInProgress)
330  {
331  MotionEvent mappedEvent = mapTouchEvent(e2);
332  sessionViewListener.onSessionViewMove((int)mappedEvent.getX(),
333  (int)mappedEvent.getY());
334  return true;
335  }
336 
337  return false;
338  }
339 
340  public boolean onDoubleTap(MotionEvent e)
341  {
342  // send 2nd click for double click
343  MotionEvent mappedEvent = mapTouchEvent(e);
344  sessionViewListener.onSessionViewLeftTouch((int)mappedEvent.getX(),
345  (int)mappedEvent.getY(), true);
346  sessionViewListener.onSessionViewLeftTouch((int)mappedEvent.getX(),
347  (int)mappedEvent.getY(), false);
348  return true;
349  }
350 
351  public boolean onSingleTapUp(MotionEvent e)
352  {
353  // send single click
354  MotionEvent mappedEvent = mapTouchEvent(e);
355  sessionViewListener.onSessionViewBeginTouch();
356  switch (e.getButtonState())
357  {
358  case MotionEvent.BUTTON_PRIMARY:
359  sessionViewListener.onSessionViewLeftTouch((int)mappedEvent.getX(),
360  (int)mappedEvent.getY(), true);
361  sessionViewListener.onSessionViewLeftTouch((int)mappedEvent.getX(),
362  (int)mappedEvent.getY(), false);
363  break;
364  case MotionEvent.BUTTON_SECONDARY:
365  sessionViewListener.onSessionViewRightTouch((int)mappedEvent.getX(),
366  (int)mappedEvent.getY(), true);
367  sessionViewListener.onSessionViewRightTouch((int)mappedEvent.getX(),
368  (int)mappedEvent.getY(), false);
369  sessionViewListener.onSessionViewLeftTouch((int)mappedEvent.getX(),
370  (int)mappedEvent.getY(), true);
371  sessionViewListener.onSessionViewLeftTouch((int)mappedEvent.getX(),
372  (int)mappedEvent.getY(), false);
373  break;
374  }
375  sessionViewListener.onSessionViewEndTouch();
376  return true;
377  }
378  }
379 
380  private class SessionDoubleGestureListener
381  implements DoubleGestureDetector.OnDoubleGestureListener
382  {
383  private MotionEvent prevEvent = null;
384 
385  public boolean onDoubleTouchDown(MotionEvent e)
386  {
387  sessionViewListener.onSessionViewBeginTouch();
388  prevEvent = MotionEvent.obtain(e);
389  return true;
390  }
391 
392  public boolean onDoubleTouchUp(MotionEvent e)
393  {
394  if (prevEvent != null)
395  {
396  prevEvent.recycle();
397  prevEvent = null;
398  }
399  sessionViewListener.onSessionViewEndTouch();
400  return true;
401  }
402 
403  public boolean onDoubleTouchScroll(MotionEvent e1, MotionEvent e2)
404  {
405  // calc if user scrolled up or down (or if any scrolling happened at all)
406  float deltaY = e2.getY() - prevEvent.getY();
407  if (deltaY > TOUCH_SCROLL_DELTA)
408  {
409  sessionViewListener.onSessionViewScroll(true);
410  prevEvent.recycle();
411  prevEvent = MotionEvent.obtain(e2);
412  }
413  else if (deltaY < -TOUCH_SCROLL_DELTA)
414  {
415  sessionViewListener.onSessionViewScroll(false);
416  prevEvent.recycle();
417  prevEvent = MotionEvent.obtain(e2);
418  }
419  return true;
420  }
421 
422  public boolean onDoubleTouchSingleTap(MotionEvent e)
423  {
424  // send single click
425  MotionEvent mappedEvent = mapDoubleTouchEvent(e);
426  sessionViewListener.onSessionViewRightTouch((int)mappedEvent.getX(),
427  (int)mappedEvent.getY(), true);
428  sessionViewListener.onSessionViewRightTouch((int)mappedEvent.getX(),
429  (int)mappedEvent.getY(), false);
430  return true;
431  }
432  }
433 
434  @Override public InputConnection onCreateInputConnection(EditorInfo outAttrs)
435  {
436  super.onCreateInputConnection(outAttrs);
437  outAttrs.inputType = InputType.TYPE_CLASS_TEXT;
438  return null;
439  }
440 }
void setScaleGestureDetector(ScaleGestureDetector scaleGestureDetector)