/* Android Session view Copyright 2013 Thincast Technologies GmbH, Author: Martin Fleisz This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */ package com.freerdp.freerdpcore.presentation; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.drawable.BitmapDrawable; import android.util.AttributeSet; import android.util.Log; import android.view.InputDevice; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.ScaleGestureDetector; import android.view.View; import com.freerdp.freerdpcore.application.SessionState; import com.freerdp.freerdpcore.services.LibFreeRDP; import com.freerdp.freerdpcore.utils.DoubleGestureDetector; import com.freerdp.freerdpcore.utils.GestureDetector; import com.freerdp.freerdpcore.utils.Mouse; import java.util.Stack; public class SessionView extends View { public static final float MAX_SCALE_FACTOR = 3.0f; public static final float MIN_SCALE_FACTOR = 1.0f; private static final String TAG = "SessionView"; private static final float SCALE_FACTOR_DELTA = 0.0001f; private static final float TOUCH_SCROLL_DELTA = 10.0f; private int width; private int height; private BitmapDrawable surface; private Stack invalidRegions; private int touchPointerPaddingWidth = 0; private int touchPointerPaddingHeight = 0; private SessionViewListener sessionViewListener = null; // helpers for scaling gesture handling private float scaleFactor = 1.0f; private Matrix scaleMatrix; private Matrix invScaleMatrix; private RectF invalidRegionF; private GestureDetector gestureDetector; private SessionState currentSession; // private static final String TAG = "FreeRDP.SessionView"; private DoubleGestureDetector doubleGestureDetector; public SessionView(Context context) { super(context); initSessionView(context); } public SessionView(Context context, AttributeSet attrs) { super(context, attrs); initSessionView(context); } public SessionView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); initSessionView(context); } private void initSessionView(Context context) { invalidRegions = new Stack(); gestureDetector = new GestureDetector(context, new SessionGestureListener(), null, true); doubleGestureDetector = new DoubleGestureDetector(context, null, new SessionDoubleGestureListener()); scaleFactor = 1.0f; scaleMatrix = new Matrix(); invScaleMatrix = new Matrix(); invalidRegionF = new RectF(); setSystemUiVisibility(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); } public void setScaleGestureDetector(ScaleGestureDetector scaleGestureDetector) { doubleGestureDetector.setScaleGestureDetector(scaleGestureDetector); } public void setSessionViewListener(SessionViewListener sessionViewListener) { this.sessionViewListener = sessionViewListener; } public void addInvalidRegion(Rect invalidRegion) { // correctly transform invalid region depending on current scaling invalidRegionF.set(invalidRegion); scaleMatrix.mapRect(invalidRegionF); invalidRegionF.roundOut(invalidRegion); invalidRegions.add(invalidRegion); } public void invalidateRegion() { invalidate(invalidRegions.pop()); } public void onSurfaceChange(SessionState session) { surface = session.getSurface(); Bitmap bitmap = surface.getBitmap(); width = bitmap.getWidth(); height = bitmap.getHeight(); surface.setBounds(0, 0, width, height); setMinimumWidth(width); setMinimumHeight(height); requestLayout(); currentSession = session; } public float getZoom() { return scaleFactor; } public void setZoom(float factor) { // calc scale matrix and inverse scale matrix (to correctly transform the view and moues // coordinates) scaleFactor = factor; scaleMatrix.setScale(scaleFactor, scaleFactor); invScaleMatrix.setScale(1.0f / scaleFactor, 1.0f / scaleFactor); // update layout requestLayout(); } public boolean isAtMaxZoom() { return (scaleFactor > (MAX_SCALE_FACTOR - SCALE_FACTOR_DELTA)); } public boolean isAtMinZoom() { return (scaleFactor < (MIN_SCALE_FACTOR + SCALE_FACTOR_DELTA)); } public boolean zoomIn(float factor) { boolean res = true; scaleFactor += factor; if (scaleFactor > (MAX_SCALE_FACTOR - SCALE_FACTOR_DELTA)) { scaleFactor = MAX_SCALE_FACTOR; res = false; } setZoom(scaleFactor); return res; } public boolean zoomOut(float factor) { boolean res = true; scaleFactor -= factor; if (scaleFactor < (MIN_SCALE_FACTOR + SCALE_FACTOR_DELTA)) { scaleFactor = MIN_SCALE_FACTOR; res = false; } setZoom(scaleFactor); return res; } public void setTouchPointerPadding(int widht, int height) { touchPointerPaddingWidth = widht; touchPointerPaddingHeight = height; requestLayout(); } public int getTouchPointerPaddingWidth() { return touchPointerPaddingWidth; } public int getTouchPointerPaddingHeight() { return touchPointerPaddingHeight; } @Override public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { Log.v(TAG, width + "x" + height); this.setMeasuredDimension((int)(width * scaleFactor) + touchPointerPaddingWidth, (int)(height * scaleFactor) + touchPointerPaddingHeight); } @Override public void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.save(); canvas.concat(scaleMatrix); surface.draw(canvas); canvas.restore(); } // dirty hack: we call back to our activity and call onBackPressed as this doesn't reach us when // the soft keyboard is shown ... @Override public boolean dispatchKeyEventPreIme(KeyEvent event) { if (event.getKeyCode() == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_DOWN) ((SessionActivity)this.getContext()).onBackPressed(); return super.dispatchKeyEventPreIme(event); } // perform mapping on the touch event's coordinates according to the current scaling private MotionEvent mapTouchEvent(MotionEvent event) { MotionEvent mappedEvent = MotionEvent.obtain(event); float[] coordinates = { mappedEvent.getX(), mappedEvent.getY() }; invScaleMatrix.mapPoints(coordinates); mappedEvent.setLocation(coordinates[0], coordinates[1]); return mappedEvent; } // perform mapping on the double touch event's coordinates according to the current scaling private MotionEvent mapDoubleTouchEvent(MotionEvent event) { MotionEvent mappedEvent = MotionEvent.obtain(event); float[] coordinates = { (mappedEvent.getX(0) + mappedEvent.getX(1)) / 2, (mappedEvent.getY(0) + mappedEvent.getY(1)) / 2 }; invScaleMatrix.mapPoints(coordinates); mappedEvent.setLocation(coordinates[0], coordinates[1]); return mappedEvent; } @Override public boolean onTouchEvent(MotionEvent event) { boolean res = gestureDetector.onTouchEvent(event); res |= doubleGestureDetector.onTouchEvent(event); return res; } public interface SessionViewListener { abstract void onSessionViewBeginTouch(); abstract void onSessionViewEndTouch(); abstract void onSessionViewLeftTouch(int x, int y, boolean down); abstract void onSessionViewRightTouch(int x, int y, boolean down); abstract void onSessionViewMove(int x, int y); abstract void onSessionViewScroll(boolean down); } private class SessionGestureListener extends GestureDetector.SimpleOnGestureListener { boolean longPressInProgress = false; public boolean onDown(MotionEvent e) { return true; } public boolean onUp(MotionEvent e) { sessionViewListener.onSessionViewEndTouch(); return true; } public void onLongPress(MotionEvent e) { MotionEvent mappedEvent = mapTouchEvent(e); sessionViewListener.onSessionViewBeginTouch(); sessionViewListener.onSessionViewLeftTouch((int)mappedEvent.getX(), (int)mappedEvent.getY(), true); longPressInProgress = true; } public void onLongPressUp(MotionEvent e) { MotionEvent mappedEvent = mapTouchEvent(e); sessionViewListener.onSessionViewLeftTouch((int)mappedEvent.getX(), (int)mappedEvent.getY(), false); longPressInProgress = false; sessionViewListener.onSessionViewEndTouch(); } public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { if (longPressInProgress) { MotionEvent mappedEvent = mapTouchEvent(e2); sessionViewListener.onSessionViewMove((int)mappedEvent.getX(), (int)mappedEvent.getY()); return true; } return false; } public boolean onDoubleTap(MotionEvent e) { // send 2nd click for double click MotionEvent mappedEvent = mapTouchEvent(e); sessionViewListener.onSessionViewLeftTouch((int)mappedEvent.getX(), (int)mappedEvent.getY(), true); sessionViewListener.onSessionViewLeftTouch((int)mappedEvent.getX(), (int)mappedEvent.getY(), false); return true; } public boolean onSingleTapUp(MotionEvent e) { // send single click MotionEvent mappedEvent = mapTouchEvent(e); sessionViewListener.onSessionViewBeginTouch(); switch (e.getButtonState()) { case MotionEvent.BUTTON_PRIMARY: sessionViewListener.onSessionViewLeftTouch((int)mappedEvent.getX(), (int)mappedEvent.getY(), true); sessionViewListener.onSessionViewLeftTouch((int)mappedEvent.getX(), (int)mappedEvent.getY(), false); break; case MotionEvent.BUTTON_SECONDARY: sessionViewListener.onSessionViewRightTouch((int)mappedEvent.getX(), (int)mappedEvent.getY(), true); sessionViewListener.onSessionViewRightTouch((int)mappedEvent.getX(), (int)mappedEvent.getY(), false); sessionViewListener.onSessionViewLeftTouch((int)mappedEvent.getX(), (int)mappedEvent.getY(), true); sessionViewListener.onSessionViewLeftTouch((int)mappedEvent.getX(), (int)mappedEvent.getY(), false); break; } sessionViewListener.onSessionViewEndTouch(); return true; } } private class SessionDoubleGestureListener implements DoubleGestureDetector.OnDoubleGestureListener { private MotionEvent prevEvent = null; public boolean onDoubleTouchDown(MotionEvent e) { sessionViewListener.onSessionViewBeginTouch(); prevEvent = MotionEvent.obtain(e); return true; } public boolean onDoubleTouchUp(MotionEvent e) { if (prevEvent != null) { prevEvent.recycle(); prevEvent = null; } sessionViewListener.onSessionViewEndTouch(); return true; } public boolean onDoubleTouchScroll(MotionEvent e1, MotionEvent e2) { // calc if user scrolled up or down (or if any scrolling happened at all) float deltaY = e2.getY() - prevEvent.getY(); if (deltaY > TOUCH_SCROLL_DELTA) { sessionViewListener.onSessionViewScroll(true); prevEvent.recycle(); prevEvent = MotionEvent.obtain(e2); } else if (deltaY < -TOUCH_SCROLL_DELTA) { sessionViewListener.onSessionViewScroll(false); prevEvent.recycle(); prevEvent = MotionEvent.obtain(e2); } return true; } public boolean onDoubleTouchSingleTap(MotionEvent e) { // send single click MotionEvent mappedEvent = mapDoubleTouchEvent(e); sessionViewListener.onSessionViewRightTouch((int)mappedEvent.getX(), (int)mappedEvent.getY(), true); sessionViewListener.onSessionViewRightTouch((int)mappedEvent.getX(), (int)mappedEvent.getY(), false); return true; } } }