summaryrefslogtreecommitdiffstats
path: root/remoting/android/java/src/org/chromium/chromoting/SimulatedTouchInputStrategy.java
blob: ccc0dbd60b6bb4af4409ce02b9a41f172fc3cd03 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
// 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.Point;
import android.os.SystemClock;
import android.view.MotionEvent;
import android.view.ViewConfiguration;

import org.chromium.chromoting.jni.Client;

/**
 * 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 Client mClient;

    /**
     * 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 Point 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 = TouchInputHandlerInterface.BUTTON_UNDEFINED;

    public SimulatedTouchInputStrategy(RenderData renderData, Client client, Context context) {
        mRenderData = renderData;
        mClient = client;

        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;

        synchronized (mRenderData) {
            mRenderData.drawCursor = false;
        }
    }

    @Override
    public boolean onTap(int button) {
        Point currentTapPoint = getCursorPosition();
        if (button == TouchInputHandlerInterface.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 Point(mLastTapPoint);
                mLastTapPoint = null;
                mLastTapTimeInMs = 0;
            } else {
                mLastTapPoint = currentTapPoint;
                mLastTapTimeInMs = SystemClock.uptimeMillis();
            }
        } else {
            mLastTapPoint = null;
            mLastTapTimeInMs = 0;
        }

        injectMouseButtonEvent(button, true, currentTapPoint);
        injectMouseButtonEvent(button, false, currentTapPoint);

        return true;
    }

    @Override
    public boolean onPressAndHold(int button) {
        injectMouseButtonEvent(button, true, getCursorPosition());
        mHeldButton = button;
        return true;
    }

    @Override
    public void onScroll(float distanceX, float distanceY) {
        mClient.sendMouseWheelEvent((int) -distanceX, (int) -distanceY);
    }

    @Override
    public void onMotionEvent(MotionEvent event) {
        if (event.getActionMasked() == MotionEvent.ACTION_UP
                && mHeldButton != TouchInputHandlerInterface.BUTTON_UNDEFINED) {
            injectMouseButtonEvent(mHeldButton, false, getCursorPosition());
            mHeldButton = TouchInputHandlerInterface.BUTTON_UNDEFINED;
        }
    }

    @Override
    public void injectCursorMoveEvent(int x, int y) {
        mClient.sendMouseEvent(x, y, TouchInputHandlerInterface.BUTTON_UNDEFINED, false);
    }

    @Override
    public DesktopView.InputFeedbackType getShortPressFeedbackType() {
        return DesktopView.InputFeedbackType.SMALL_ANIMATION;
    }

    @Override
    public DesktopView.InputFeedbackType getLongPressFeedbackType() {
        return DesktopView.InputFeedbackType.LARGE_ANIMATION;
    }

    @Override
    public boolean isIndirectInputMode() {
        return false;
    }

    private Point getCursorPosition() {
        synchronized (mRenderData) {
            return mRenderData.getCursorPosition();
        }
    }

    private boolean isDoubleTap(int currentX, int 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};
        synchronized (mRenderData) {
            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);
    }

    private void injectMouseButtonEvent(int button, boolean pressed, Point tapPoint) {
        mClient.sendMouseEvent(tapPoint.x, tapPoint.y, button, pressed);
    }
}