Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit b797f6bb authored by Dieter Hsu's avatar Dieter Hsu Committed by Android (Google) Code Review
Browse files

Merge "Add TouchExplorerTest"

parents f5e4b2b2 01f426fd
Loading
Loading
Loading
Loading
+25 −13
Original line number Diff line number Diff line
@@ -145,7 +145,7 @@ class AccessibilityGestureDetector extends GestureDetector.SimpleOnGestureListen

    private final Listener mListener;
    private final Context mContext;  // Retained for on-demand construction of GestureDetector.
    protected GestureDetector mGestureDetector;  // Double-tap detector. Visible for test.
    private final GestureDetector mGestureDetector;  // Double-tap detector.

    // Indicates that a single tap has occurred.
    private boolean mFirstTapDetected;
@@ -216,10 +216,34 @@ class AccessibilityGestureDetector extends GestureDetector.SimpleOnGestureListen
    // cancelled.
    private static final long CANCEL_ON_PAUSE_THRESHOLD_STARTED_MS = 300;

    /**
     * Construct the gesture detector for {@link TouchExplorer}.
     *
     * @see #AccessibilityGestureDetector(Context, Listener, GestureDetector)
     */
    AccessibilityGestureDetector(Context context, Listener listener) {
        this(context, listener, null);
    }

    /**
     * Construct the gesture detector for {@link TouchExplorer}.
     *
     * @param context A context handle for accessing resources.
     * @param listener A listener to callback with gesture state or information.
     * @param detector The gesture detector to handle touch event. If null the default one created
     *                 in place, or for testing purpose.
     */
    AccessibilityGestureDetector(Context context, Listener listener, GestureDetector detector) {
        mListener = listener;
        mContext = context;

        // Break the circular dependency between constructors and let the class to be testable
        if (detector == null) {
            mGestureDetector = new GestureDetector(context, this);
        } else {
            mGestureDetector = detector;
        }
        mGestureDetector.setOnDoubleTapListener(this);
        mGestureDetectionThreshold = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_MM, 1,
                context.getResources().getDisplayMetrics()) * GESTURE_CONFIRM_MM;

@@ -244,18 +268,6 @@ class AccessibilityGestureDetector extends GestureDetector.SimpleOnGestureListen
     * @return true if the event is consumed, else false
     */
    public boolean onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {

        // Construct GestureDetector double-tap detector on demand, so that testable sub-class
        // can use mock GestureDetector.
        // TODO: Break the circular dependency between GestureDetector's constructor and
        // AccessibilityGestureDetector's constructor. Construct GestureDetector in TouchExplorer,
        // using a GestureDetector listener owned by TouchExplorer, which passes double-tap state
        // information to AccessibilityGestureDetector.
        if (mGestureDetector == null) {
            mGestureDetector = new GestureDetector(mContext, this);
            mGestureDetector.setOnDoubleTapListener(this);
        }

        // The accessibility gesture detector is interested in the movements in physical space,
        // so it uses the rawEvent to ignore magnification and other transformations.
        final float x = rawEvent.getX();
+16 −2
Original line number Diff line number Diff line
@@ -165,8 +165,9 @@ class TouchExplorer extends BaseEventStreamTransformation
    /**
     * Creates a new instance.
     *
     * @param inputFilter The input filter associated with this explorer.
     * @param context A context handle for accessing resources.
     * @param service The service to notify touch interaction and gesture completed and to perform
     *                action.
     */
    public TouchExplorer(Context context, AccessibilityManagerService service) {
        mContext = context;
@@ -1311,7 +1312,20 @@ class TouchExplorer extends BaseEventStreamTransformation

    @Override
    public String toString() {
        return LOG_TAG;
        return "TouchExplorer { " +
                "mCurrentState: " + getStateSymbolicName(mCurrentState) +
                ", mDetermineUserIntentTimeout: " + mDetermineUserIntentTimeout +
                ", mDoubleTapSlop: " + mDoubleTapSlop +
                ", mDraggingPointerId: " + mDraggingPointerId +
                ", mLongPressingPointerId: " + mLongPressingPointerId +
                ", mLongPressingPointerDeltaX: " + mLongPressingPointerDeltaX +
                ", mLongPressingPointerDeltaY: " + mLongPressingPointerDeltaY +
                ", mLastTouchedWindowId: " + mLastTouchedWindowId +
                ", mScaledMinPointerDistanceToUseMiddleLocation: "
                + mScaledMinPointerDistanceToUseMiddleLocation +
                ", mTempPoint: " + mTempPoint +
                ", mTouchExplorationInProgress: " + mTouchExplorationInProgress +
                " }";
    }

    class InjectedPointerTracker {
+2 −18
Original line number Diff line number Diff line
@@ -46,23 +46,8 @@ public class AccessibilityGestureDetectorTest {
    private static final int PATH_STEP_PIXELS = 200;
    private static final long PATH_STEP_MILLISEC = 100;

    /**
     * AccessibilitGestureDetector that can mock double-tap detector.
     */
    private class AccessibilityGestureDetectorTestable extends AccessibilityGestureDetector {
        public AccessibilityGestureDetectorTestable(Context context, Listener listener) {
            super(context, listener);
        }

        protected void setDoubleTapDetector(GestureDetector gestureDetector) {
            mGestureDetector = gestureDetector;
            mGestureDetector.setOnDoubleTapListener(this);
        }
    }


    // Data used by all tests
    private AccessibilityGestureDetectorTestable mDetector;
    private AccessibilityGestureDetector mDetector;
    private AccessibilityGestureDetector.Listener mResultListener;


@@ -87,9 +72,8 @@ public class AccessibilityGestureDetectorTest {

        // Construct a testable AccessibilityGestureDetector.
        mResultListener = mock(AccessibilityGestureDetector.Listener.class);
        mDetector = new AccessibilityGestureDetectorTestable(contextMock, mResultListener);
        GestureDetector doubleTapDetectorMock = mock(GestureDetector.class);
        mDetector.setDoubleTapDetector(doubleTapDetectorMock);
        mDetector = new AccessibilityGestureDetector(contextMock, mResultListener, doubleTapDetectorMock);
    }


+327 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2018 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.server.accessibility;

import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertEquals;

import android.content.Context;
import android.graphics.PointF;
import android.os.SystemClock;
import android.util.DebugUtils;
import android.view.InputDevice;
import android.view.MotionEvent;

import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;

import java.util.ArrayList;
import java.util.List;

@RunWith(AndroidJUnit4.class)
public class TouchExplorerTest {

    public static final int STATE_TOUCH_EXPLORING = 0x00000001;
    public static final int STATE_DRAGGING = 0x00000002;
    public static final int STATE_DELEGATING = 0x00000004;

    private static final int FLAG_1FINGER = 0x8000;
    private static final int FLAG_2FINGERS = 0x0100;
    private static final int FLAG_3FINGERS = 0x0200;
    private static final int FLAG_MOVING = 0x00010000;
    private static final int FLAG_MOVING_DIFF_DIRECTION = 0x00020000;

    private static final int STATE_TOUCH_EXPLORING_1FINGER = STATE_TOUCH_EXPLORING | FLAG_1FINGER;
    private static final int STATE_TOUCH_EXPLORING_2FINGER = STATE_TOUCH_EXPLORING | FLAG_2FINGERS;
    private static final int STATE_TOUCH_EXPLORING_3FINGER = STATE_TOUCH_EXPLORING | FLAG_3FINGERS;
    private static final int STATE_MOVING_2FINGERS = STATE_TOUCH_EXPLORING_2FINGER | FLAG_MOVING;
    private static final int STATE_MOVING_3FINGERS = STATE_TOUCH_EXPLORING_3FINGER | FLAG_MOVING;
    private static final int STATE_DRAGGING_2FINGERS = STATE_DRAGGING | FLAG_2FINGERS;
    private static final int STATE_PINCH_2FINGERS =
            STATE_TOUCH_EXPLORING_2FINGER | FLAG_MOVING_DIFF_DIRECTION;
    private static final float DEFAULT_X = 301f;
    private static final float DEFAULT_Y = 299f;

    private EventStreamTransformation mCaptor;
    private MotionEvent mLastEvent;
    private TouchExplorer mTouchExplorer;
    private long mLastDownTime = Integer.MIN_VALUE;

    /**
     * {@link TouchExplorer#sendDownForAllNotInjectedPointers} injecting events with the same object
     * is resulting {@link ArgumentCaptor} to capture events with last state. Before implementation
     * change, this helper class will save copies to verify the result.
     */
    private class EventCaptor implements EventStreamTransformation {
        List<MotionEvent> mEvents = new ArrayList<>();

        @Override
        public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
            mEvents.add(0, event.copy());
        }

        @Override
        public void setNext(EventStreamTransformation next) {
        }

        @Override
        public EventStreamTransformation getNext() {
            return null;
        }
    }

    @Before
    public void setUp() {
        Context context = InstrumentationRegistry.getContext();
        AccessibilityManagerService ams = new AccessibilityManagerService(context);
        mCaptor = new EventCaptor();
        mTouchExplorer = new TouchExplorer(context, ams);
        mTouchExplorer.setNext(mCaptor);
    }

    @Test
    public void testTwoFingersMove_shouldDelegatingAndInjectActionDownPointerDown() {
        goFromStateIdleTo(STATE_MOVING_2FINGERS);

        assertState(STATE_DELEGATING);
        assertCapturedEvents(
                MotionEvent.ACTION_DOWN,
                MotionEvent.ACTION_POINTER_DOWN);
        assertCapturedEventsNoHistory();
    }

    @Test
    public void testTwoFingersDrag_shouldDraggingAndActionDown() {
        goFromStateIdleTo(STATE_DRAGGING_2FINGERS);

        assertState(STATE_DRAGGING);
        assertCapturedEvents(MotionEvent.ACTION_DOWN);
        assertCapturedEventsNoHistory();
    }

    @Test
    public void testTwoFingersNotDrag_shouldDelegatingAndActionUpDownPointerDown() {
        // only from dragging state, and withMoveHistory no dragging
        goFromStateIdleTo(STATE_PINCH_2FINGERS);

        assertState(STATE_DELEGATING);
        assertCapturedEvents(
                /* goto dragging state */ MotionEvent.ACTION_DOWN,
                /* leave dragging state */ MotionEvent.ACTION_UP,
                MotionEvent.ACTION_DOWN,
                MotionEvent.ACTION_POINTER_DOWN);
        assertCapturedEventsNoHistory();
    }

    @Test
    public void testThreeFingersMove_shouldDelegatingAnd3ActionPointerDown() {
        goFromStateIdleTo(STATE_MOVING_3FINGERS);

        assertState(STATE_DELEGATING);
        assertCapturedEvents(
                MotionEvent.ACTION_DOWN,
                MotionEvent.ACTION_POINTER_DOWN,
                MotionEvent.ACTION_POINTER_DOWN);
        assertCapturedEventsNoHistory();
    }

    private static MotionEvent fromTouchscreen(MotionEvent ev) {
        ev.setSource(InputDevice.SOURCE_TOUCHSCREEN);
        return ev;
    }

    private static PointF p(int x, int y) {
        return new PointF(x, y);
    }

    private static String stateToString(int state) {
        return DebugUtils.valueToString(TouchExplorerTest.class, "STATE_", state);
    }

    private void goFromStateIdleTo(int state) {
        try {
            switch (state) {
                case STATE_TOUCH_EXPLORING: {
                    mTouchExplorer.onDestroy();
                }
                break;
                case STATE_TOUCH_EXPLORING_1FINGER: {
                    goFromStateIdleTo(STATE_TOUCH_EXPLORING);
                    send(downEvent());
                }
                break;
                case STATE_TOUCH_EXPLORING_2FINGER: {
                    goFromStateIdleTo(STATE_TOUCH_EXPLORING_1FINGER);
                    send(pointerDownEvent());
                }
                break;
                case STATE_TOUCH_EXPLORING_3FINGER: {
                    goFromStateIdleTo(STATE_TOUCH_EXPLORING_2FINGER);
                    send(thirdPointerDownEvent());
                }
                break;
                case STATE_MOVING_2FINGERS: {
                    goFromStateIdleTo(STATE_TOUCH_EXPLORING_2FINGER);
                    moveEachPointers(mLastEvent, p(10, 0), p(5, 10));
                    send(mLastEvent);
                }
                break;
                case STATE_DRAGGING_2FINGERS: {
                    goFromStateIdleTo(STATE_TOUCH_EXPLORING_2FINGER);
                    moveEachPointers(mLastEvent, p(10, 0), p(10, 0));
                    send(mLastEvent);
                }
                break;
                case STATE_PINCH_2FINGERS: {
                    goFromStateIdleTo(STATE_DRAGGING_2FINGERS);
                    moveEachPointers(mLastEvent, p(10, 0), p(-10, 1));
                    send(mLastEvent);
                }
                break;
                case STATE_MOVING_3FINGERS: {
                    goFromStateIdleTo(STATE_TOUCH_EXPLORING_3FINGER);
                    moveEachPointers(mLastEvent, p(1, 0), p(1, 0), p(1, 0));
                    send(mLastEvent);
                }
                break;
                default:
                    throw new IllegalArgumentException("Illegal state: " + state);
            }
        } catch (Throwable t) {
            throw new RuntimeException("Failed to go to state " + stateToString(state), t);
        }
    }

    private void send(MotionEvent event) {
        final MotionEvent sendEvent = fromTouchscreen(event);
        mLastEvent = sendEvent;
        try {
            mTouchExplorer.onMotionEvent(sendEvent, sendEvent, /* policyFlags */ 0);
        } catch (Throwable t) {
            throw new RuntimeException("Exception while handling " + sendEvent, t);
        }
    }

    private void assertState(int expect) {
        final String expectState = "STATE_" + stateToString(expect);
        assertTrue(String.format("Expect state: %s, but: %s", expectState, mTouchExplorer),
                mTouchExplorer.toString().contains(expectState));
    }

    private void assertCapturedEvents(int... actionsInOrder) {
        final int eventCount = actionsInOrder.length;
        assertEquals(eventCount, getCapturedEvents().size());
        for (int i = 0; i < eventCount; i++) {
            assertEquals(actionsInOrder[eventCount - i - 1], getCapturedEvent(i).getActionMasked());
        }
    }

    private void assertCapturedEventsNoHistory() {
        for (MotionEvent e : getCapturedEvents()) {
            assertEquals(0, e.getHistorySize());
        }
    }

    private MotionEvent getCapturedEvent(int index) {
        return getCapturedEvents().get(index);
    }

    private List<MotionEvent> getCapturedEvents() {
        return ((EventCaptor) mCaptor).mEvents;
    }

    private MotionEvent downEvent() {
        mLastDownTime = SystemClock.uptimeMillis();
        return fromTouchscreen(
                MotionEvent.obtain(mLastDownTime, mLastDownTime, MotionEvent.ACTION_DOWN, DEFAULT_X,
                        DEFAULT_Y, 0));
    }

    private MotionEvent pointerDownEvent() {
        final int secondPointerId = 0x0100;
        final int action = MotionEvent.ACTION_POINTER_DOWN | secondPointerId;
        final float[] x = new float[]{DEFAULT_X, DEFAULT_X + 29};
        final float[] y = new float[]{DEFAULT_Y, DEFAULT_Y + 28};
        return manyPointerEvent(action, x, y);
    }

    private MotionEvent thirdPointerDownEvent() {
        final int thirdPointerId = 0x0200;
        final int action = MotionEvent.ACTION_POINTER_DOWN | thirdPointerId;
        final float[] x = new float[]{DEFAULT_X, DEFAULT_X + 29, DEFAULT_X + 59};
        final float[] y = new float[]{DEFAULT_Y, DEFAULT_Y + 28, DEFAULT_Y + 58};
        return manyPointerEvent(action, x, y);
    }

    private void moveEachPointers(MotionEvent event, PointF... points) {
        final float[] x = new float[points.length];
        final float[] y = new float[points.length];
        for (int i = 0; i < points.length; i++) {
            x[i] = event.getX(i) + points[i].x;
            y[i] = event.getY(i) + points[i].y;
        }
        MotionEvent newEvent = manyPointerEvent(MotionEvent.ACTION_MOVE, x, y);
        event.setAction(MotionEvent.ACTION_MOVE);
        // add history count
        event.addBatch(newEvent);
    }

    private MotionEvent manyPointerEvent(int action, float[] x, float[] y) {
        return manyPointerEvent(action, x, y, mLastDownTime);
    }

    private MotionEvent manyPointerEvent(int action, float[] x, float[] y, long downTime) {
        final int len = x.length;

        final MotionEvent.PointerProperties[] pp = new MotionEvent.PointerProperties[len];
        for (int i = 0; i < len; i++) {
            MotionEvent.PointerProperties pointerProperty = new MotionEvent.PointerProperties();
            pointerProperty.id = i;
            pointerProperty.toolType = MotionEvent.TOOL_TYPE_FINGER;
            pp[i] = pointerProperty;
        }

        final MotionEvent.PointerCoords[] pc = new MotionEvent.PointerCoords[len];
        for (int i = 0; i < len; i++) {
            MotionEvent.PointerCoords pointerCoord = new MotionEvent.PointerCoords();
            pointerCoord.x = x[i];
            pointerCoord.y = y[i];
            pc[i] = pointerCoord;
        }

        return MotionEvent.obtain(
                /* downTime */ SystemClock.uptimeMillis(),
                /* eventTime */ downTime,
                /* action */ action,
                /* pointerCount */ pc.length,
                /* pointerProperties */ pp,
                /* pointerCoords */ pc,
                /* metaState */ 0,
                /* buttonState */ 0,
                /* xPrecision */ 1.0f,
                /* yPrecision */ 1.0f,
                /* deviceId */ 0,
                /* edgeFlags */ 0,
                /* source */ InputDevice.SOURCE_TOUCHSCREEN,
                /* flags */ 0);
    }
}