blob: 65f17304bf3b78756e63c49493f316dc9632ca21 [file] [log] [blame]
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chromoting;
import android.content.Context;
import android.graphics.PointF;
import android.os.SystemClock;
import android.view.MotionEvent;
import android.view.ViewConfiguration;
/**
* This class receives local touch events and translates them into the appropriate mouse based
* events for the remote host. The net result is that the local input method feels like a touch
* interface but the remote host will be given mouse events to inject.
*/
public class SimulatedTouchInputStrategy implements InputStrategyInterface {
/** Used to adjust the size of the region used for double tap detection. */
private static final float DOUBLE_TAP_SLOP_SCALE_FACTOR = 0.25f;
private final RenderData mRenderData;
private final InputEventSender mInjector;
/**
* Stores the time of the most recent left button single tap processed.
*/
private long mLastTapTimeInMs = 0;
/**
* Stores the position of the last left button single tap processed.
*/
private PointF mLastTapPoint;
/**
* The maximum distance, in pixels, between two points in order for them to be considered a
* double tap gesture.
*/
private final int mDoubleTapSlopSquareInPx;
/**
* The interval, measured in milliseconds, in which two consecutive left button taps must
* occur in order to be considered a double tap gesture.
*/
private final long mDoubleTapDurationInMs;
/** Mouse-button currently held down, or BUTTON_UNDEFINED otherwise. */
private int mHeldButton = InputStub.BUTTON_UNDEFINED;
public SimulatedTouchInputStrategy(
RenderData renderData, InputEventSender injector, Context context) {
Preconditions.notNull(injector);
mRenderData = renderData;
mInjector = injector;
ViewConfiguration config = ViewConfiguration.get(context);
mDoubleTapDurationInMs = config.getDoubleTapTimeout();
// In order to detect whether the user is attempting to double tap a target, we define a
// region around the first point within which the second tap must occur. The standard way
// to do this in an Android UI (meaning a UI comprised of UI elements which conform to the
// visual guidelines for the platform which are 'Touch Friendly') is to use the
// getScaledDoubleTapSlop() value for checking this distance (or use a GestureDetector).
// Our scenario is a bit different as our UI consists of an image of a remote machine where
// the UI elements were probably designed for mouse and keyboard (meaning smaller targets)
// and the image itself which can be zoomed to change the size of the targets. Ths adds up
// to the target to be invoked often being either larger or much smaller than a standard
// Android UI element. Our approach to this problem is to make double-tap detection
// consistent regardless of the zoom level or remote target size so that the user can rely
// on their muscle memory when interacting with our UI. With respect to the original
// problem, getScaledDoubleTapSlop() gives a value which is optimized for an Android based
// UI however this value is too large for interacting with remote elements in our app.
// Our solution is to use the original value from getScaledDoubleTapSlop() (which includes
// scaling to account for display differences between devices) and apply a fudge/scale
// factor to make the interaction more intuitive and useful for our scenario.
int scaledDoubleTapSlopInPx = config.getScaledDoubleTapSlop();
scaledDoubleTapSlopInPx *= DOUBLE_TAP_SLOP_SCALE_FACTOR;
mDoubleTapSlopSquareInPx = scaledDoubleTapSlopInPx * scaledDoubleTapSlopInPx;
mRenderData.drawCursor = false;
}
@Override
public boolean onTap(int button) {
PointF currentTapPoint = getCursorPosition();
if (button == InputStub.BUTTON_LEFT) {
// Left clicks are handled a little differently than the events for other buttons.
// This is needed because translating touch events to mouse events has a problem with
// location consistency for double clicks. If you take the center location of each tap
// and inject them as mouse clicks, the distance between those two points will often
// cause the remote OS to recognize the gesture as two distinct clicks instead of a
// double click. In order to increase the success rate of double taps/clicks, we
// squirrel away the time and coordinates of each single tap and if we detect the user
// attempting a double tap, we use the original event's location for that second tap.
long tapInterval = SystemClock.uptimeMillis() - mLastTapTimeInMs;
if (isDoubleTap(currentTapPoint.x, currentTapPoint.y, tapInterval)) {
currentTapPoint = new PointF(mLastTapPoint.x, mLastTapPoint.y);
mLastTapPoint = null;
mLastTapTimeInMs = 0;
} else {
mLastTapPoint = currentTapPoint;
mLastTapTimeInMs = SystemClock.uptimeMillis();
}
} else {
mLastTapPoint = null;
mLastTapTimeInMs = 0;
}
mInjector.sendMouseClick(currentTapPoint, button);
return true;
}
@Override
public boolean onPressAndHold(int button) {
mInjector.sendMouseDown(getCursorPosition(), button);
mHeldButton = button;
return true;
}
@Override
public void onScroll(float distanceX, float distanceY) {
mInjector.sendReverseMouseWheelEvent(distanceX, distanceY);
}
@Override
public void onMotionEvent(MotionEvent event) {
if (event.getActionMasked() == MotionEvent.ACTION_UP
&& mHeldButton != InputStub.BUTTON_UNDEFINED) {
mInjector.sendMouseUp(getCursorPosition(), mHeldButton);
mHeldButton = InputStub.BUTTON_UNDEFINED;
}
}
@Override
public void injectCursorMoveEvent(int x, int y) {
mInjector.sendCursorMove(x, y);
}
@Override
public RenderStub.InputFeedbackType getShortPressFeedbackType() {
return RenderStub.InputFeedbackType.SHORT_TOUCH_ANIMATION;
}
@Override
public RenderStub.InputFeedbackType getLongPressFeedbackType() {
return RenderStub.InputFeedbackType.LONG_TOUCH_ANIMATION;
}
@Override
public boolean isIndirectInputMode() {
return false;
}
private PointF getCursorPosition() {
return mRenderData.getCursorPosition();
}
private boolean isDoubleTap(float currentX, float currentY, long tapInterval) {
if (tapInterval > mDoubleTapDurationInMs || mLastTapPoint == null) {
return false;
}
// Convert the image based coordinates back to screen coordinates so the user experiences
// consistent double tap behavior regardless of zoom level.
//
float[] currentValues = {currentX, currentY};
float[] previousValues = {mLastTapPoint.x, mLastTapPoint.y};
mRenderData.transform.mapPoints(currentValues);
mRenderData.transform.mapPoints(previousValues);
int deltaX = (int) (currentValues[0] - previousValues[0]);
int deltaY = (int) (currentValues[1] - previousValues[1]);
return ((deltaX * deltaX + deltaY * deltaY) <= mDoubleTapSlopSquareInPx);
}
}