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

Commit 01f426fd authored by Dieter Hsu's avatar Dieter Hsu
Browse files

Add TouchExplorerTest

- Add test cases to check sent action-change-to-non-move motion events
  is not contain history.
- Fix AccessilityGestureDetector TODO that will affect atest test fail

Bug: 74069091
Test: atest FrameworksServicesTests:TouchExplorerTest FrameworksServicesTests:AccessibilityGestureDetectorTest

Change-Id: Iae4321eaeea15021dfed4503da4665398d899c07
parent 0a2348f8
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);
    }
}