Add TouchInteractionController API to allow gesture detection from the service.

Bug: 159651900
Test: atest TouchExplorationControllerTest
Change-Id: Ie28dccb0ed29e632bcdb057e909093fa5b7065ac
diff --git a/core/api/current.txt b/core/api/current.txt
index ec929ee..966a449 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -2994,6 +2994,7 @@
   }
 
   public final class AccessibilityGestureEvent implements android.os.Parcelable {
+    ctor public AccessibilityGestureEvent(int, int, @NonNull java.util.List<android.view.MotionEvent>);
     method public int describeContents();
     method @NonNull public static String gestureIdToString(int);
     method public int getDisplayId();
@@ -3016,6 +3017,7 @@
     method public final android.accessibilityservice.AccessibilityServiceInfo getServiceInfo();
     method @NonNull public final android.accessibilityservice.AccessibilityService.SoftKeyboardController getSoftKeyboardController();
     method @NonNull public final java.util.List<android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction> getSystemActions();
+    method @NonNull public final android.accessibilityservice.TouchInteractionController getTouchInteractionController(int);
     method public java.util.List<android.view.accessibility.AccessibilityWindowInfo> getWindows();
     method @NonNull public final android.util.SparseArray<java.util.List<android.view.accessibility.AccessibilityWindowInfo>> getWindowsOnAllDisplays();
     method public abstract void onAccessibilityEvent(android.view.accessibility.AccessibilityEvent);
@@ -3250,6 +3252,31 @@
     method public boolean willContinue();
   }
 
+  public final class TouchInteractionController {
+    method public void addListener(@Nullable java.util.concurrent.Executor, @NonNull android.accessibilityservice.TouchInteractionController.Listener);
+    method public int getDisplayId();
+    method public int getMaxPointerCount();
+    method public int getState();
+    method public void performClick();
+    method public void performLongClickAndStartDrag();
+    method public void removeAllListeners();
+    method public boolean removeListener(@NonNull android.accessibilityservice.TouchInteractionController.Listener);
+    method public void requestDelegating();
+    method public void requestDragging(int);
+    method public void requestTouchExploration();
+    method @NonNull public static String stateToString(int);
+    field public static final int STATE_CLEAR = 0; // 0x0
+    field public static final int STATE_DELEGATING = 4; // 0x4
+    field public static final int STATE_DRAGGING = 3; // 0x3
+    field public static final int STATE_TOUCH_EXPLORING = 2; // 0x2
+    field public static final int STATE_TOUCH_INTERACTING = 1; // 0x1
+  }
+
+  public static interface TouchInteractionController.Listener {
+    method public void onMotionEvent(@NonNull android.view.MotionEvent);
+    method public void onStateChanged(int);
+  }
+
 }
 
 package android.accounts {
diff --git a/core/java/android/accessibilityservice/AccessibilityGestureEvent.java b/core/java/android/accessibilityservice/AccessibilityGestureEvent.java
index 768ec38..3c9b232 100644
--- a/core/java/android/accessibilityservice/AccessibilityGestureEvent.java
+++ b/core/java/android/accessibilityservice/AccessibilityGestureEvent.java
@@ -150,7 +150,12 @@
     private final int mDisplayId;
     private List<MotionEvent> mMotionEvents = new ArrayList<>();
 
-    /** @hide */
+/**
+ * Constructs an AccessibilityGestureEvent to be dispatched to an accessibility service.
+ * @param gestureId the id number of the gesture.
+ * @param displayId the display on which this gesture was performed.
+ * @param motionEvents the motion events that lead to this gesture.
+ */
     public AccessibilityGestureEvent(
             int gestureId, int displayId, @NonNull List<MotionEvent> motionEvents) {
         mGestureId = gestureId;
diff --git a/core/java/android/accessibilityservice/AccessibilityService.java b/core/java/android/accessibilityservice/AccessibilityService.java
index 61d2b4b..0f852b4 100644
--- a/core/java/android/accessibilityservice/AccessibilityService.java
+++ b/core/java/android/accessibilityservice/AccessibilityService.java
@@ -53,6 +53,7 @@
 import android.util.SparseArray;
 import android.view.Display;
 import android.view.KeyEvent;
+import android.view.MotionEvent;
 import android.view.SurfaceView;
 import android.view.WindowManager;
 import android.view.WindowManagerImpl;
@@ -65,6 +66,7 @@
 import com.android.internal.os.HandlerCaller;
 import com.android.internal.os.SomeArgs;
 import com.android.internal.util.Preconditions;
+import com.android.internal.util.function.pooled.PooledLambda;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -582,6 +584,10 @@
         /** Magnification changed callbacks for different displays */
         void onMagnificationChanged(int displayId, @NonNull Region region,
                 float scale, float centerX, float centerY);
+        /** Callbacks for receiving motion events. */
+        void onMotionEvent(MotionEvent event);
+        /** Callback for tuch state changes. */
+        void onTouchStateChanged(int displayId, int state);
         void onSoftKeyboardShowModeChanged(int showMode);
         void onPerformGestureResult(int sequence, boolean completedSuccessfully);
         void onFingerprintCapturingGesturesChanged(boolean active);
@@ -720,6 +726,12 @@
     /** List of magnification controllers, mapping from displayId -> MagnificationController. */
     private final SparseArray<MagnificationController> mMagnificationControllers =
             new SparseArray<>(0);
+    /**
+     * List of touch interaction controllers, mapping from displayId -> TouchInteractionController.
+     */
+    private final SparseArray<TouchInteractionController> mTouchInteractionControllers =
+            new SparseArray<>(0);
+
     private SoftKeyboardController mSoftKeyboardController;
     private final SparseArray<AccessibilityButtonController> mAccessibilityButtonControllers =
             new SparseArray<>(0);
@@ -1194,6 +1206,10 @@
         getFingerprintGestureController().onGesture(gesture);
     }
 
+    int getConnectionId() {
+        return mConnectionId;
+    }
+
     /**
      * Used to control and query the state of display magnification.
      */
@@ -2210,6 +2226,16 @@
             }
 
             @Override
+            public void onMotionEvent(MotionEvent event) {
+                AccessibilityService.this.onMotionEvent(event);
+            }
+
+            @Override
+            public void onTouchStateChanged(int displayId, int state) {
+                AccessibilityService.this.onTouchStateChanged(displayId, state);
+            }
+
+            @Override
             public void onSoftKeyboardShowModeChanged(int showMode) {
                 AccessibilityService.this.onSoftKeyboardShowModeChanged(showMode);
             }
@@ -2372,6 +2398,21 @@
         }
 
         @Override
+        public void onMotionEvent(MotionEvent event) {
+            final Message message = PooledLambda.obtainMessage(
+                            Callbacks::onMotionEvent, mCallback, event);
+            mCaller.sendMessage(message);
+        }
+
+        @Override
+        public void onTouchStateChanged(int displayId, int state) {
+            final Message message = PooledLambda.obtainMessage(Callbacks::onTouchStateChanged,
+                    mCallback,
+                    displayId, state);
+            mCaller.sendMessage(message);
+        }
+
+        @Override
         public void executeMessage(Message message) {
             switch (message.what) {
                 case DO_ON_ACCESSIBILITY_EVENT: {
@@ -2523,7 +2564,7 @@
                     }
                     return;
                 }
-                default :
+                default:
                     Log.w(LOG_TAG, "Unknown message type " + message.what);
             }
         }
@@ -2748,4 +2789,47 @@
             }
         }
     }
+
+    /**
+     * Returns the touch interaction controller for the specified logical display, which may be used
+     * to detect gestures and otherwise control touch interactions. If
+     * {@link AccessibilityServiceInfo#FLAG_REQUEST_TOUCH_EXPLORATION_MODE} is disabled the
+     * controller's methods will have no effect.
+     *
+     * @param displayId The logical display id, use {@link Display#DEFAULT_DISPLAY} for default
+     *                      display.
+     * @return the TouchExploration controller
+     */
+    @NonNull
+    public final TouchInteractionController getTouchInteractionController(int displayId) {
+        synchronized (mLock) {
+            TouchInteractionController controller = mTouchInteractionControllers.get(displayId);
+            if (controller == null) {
+                controller = new TouchInteractionController(this, mLock, displayId);
+                mTouchInteractionControllers.put(displayId, controller);
+            }
+            return controller;
+        }
+    }
+
+    void onMotionEvent(MotionEvent event) {
+        TouchInteractionController controller;
+        synchronized (mLock) {
+            int displayId = event.getDisplayId();
+            controller = mTouchInteractionControllers.get(displayId);
+        }
+        if (controller != null) {
+            controller.onMotionEvent(event);
+        }
+    }
+
+    void onTouchStateChanged(int displayId, int state) {
+        TouchInteractionController controller;
+        synchronized (mLock) {
+            controller = mTouchInteractionControllers.get(displayId);
+        }
+        if (controller != null) {
+            controller.onStateChanged(state);
+        }
+    }
 }
diff --git a/core/java/android/accessibilityservice/IAccessibilityServiceClient.aidl b/core/java/android/accessibilityservice/IAccessibilityServiceClient.aidl
index 58b0d19..651c50f 100644
--- a/core/java/android/accessibilityservice/IAccessibilityServiceClient.aidl
+++ b/core/java/android/accessibilityservice/IAccessibilityServiceClient.aidl
@@ -22,6 +22,7 @@
 import android.view.accessibility.AccessibilityWindowInfo;
 import android.accessibilityservice.AccessibilityGestureEvent;
 import android.view.KeyEvent;
+import android.view.MotionEvent;
 
 /**
  * Top-level interface to an accessibility service component.
@@ -44,6 +45,10 @@
 
     void onMagnificationChanged(int displayId, in Region region, float scale, float centerX, float centerY);
 
+    void onMotionEvent(in MotionEvent event);
+
+    void onTouchStateChanged(int displayId, int state);
+
     void onSoftKeyboardShowModeChanged(int showMode);
 
     void onPerformGestureResult(int sequence, boolean completedSuccessfully);
diff --git a/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl b/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl
index 6c360e5..81457eb 100644
--- a/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl
+++ b/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl
@@ -122,4 +122,16 @@
 
     oneway void logTrace(long timestamp, String where, long loggingTypes, String callingParams,
         int processId, long threadId, int callingUid, in Bundle serializedCallingStackInBundle);
+
+    void setServiceDetectsGesturesEnabled(int displayId, boolean mode);
+
+    void requestTouchExploration(int displayId);
+
+    void requestDragging(int displayId, int pointerId);
+
+    void requestDelegating(int displayId);
+
+    void onDoubleTap(int displayId);
+
+    void onDoubleTapAndHold(int displayId);
 }
diff --git a/core/java/android/accessibilityservice/TouchInteractionController.java b/core/java/android/accessibilityservice/TouchInteractionController.java
new file mode 100644
index 0000000..d9be49a
--- /dev/null
+++ b/core/java/android/accessibilityservice/TouchInteractionController.java
@@ -0,0 +1,425 @@
+/*
+ * Copyright (C) 2021 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 android.accessibilityservice;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.RemoteException;
+import android.util.ArrayMap;
+import android.view.MotionEvent;
+import android.view.accessibility.AccessibilityInteractionClient;
+
+import java.util.concurrent.Executor;
+
+/**
+ * This class allows a service to handle touch exploration and the detection of specialized
+ * accessibility gestures. The service receives motion events and can match those motion events
+ * against the gestures it supports. The service can also request the framework enter three other
+ * states of operation for the duration of this interaction. Upon entering any of these states the
+ * framework will take over and the service will not receive motion events until the start of a new
+ * interaction. The states are as follows:
+ *
+ * <ul>
+ *   <li>The service can tell the framework that this interaction is touch exploration. The user is
+ *       trying to explore the screen rather than manipulate it. The framework will then convert the
+ *       motion events to hover events to support touch exploration.
+ *   <li>The service can tell the framework that this interaction is a dragging interaction where
+ *       two fingers are used to execute a one-finger gesture such as scrolling the screen. The
+ *       service must specify which of the two fingers should be passed through to rest of the input
+ *       pipeline.
+ *   <li>Finally, the service can request that the framework delegate this interaction, meaning pass
+ *       it through to the rest of the input pipeline as-is.
+ * </ul>
+ *
+ * When {@link AccessibilityServiceInfo#FLAG_REQUEST_TOUCH_EXPLORATION_MODE } is enabled, this
+ * controller will receive all motion events received by the framework for the specified display
+ * when not touch-exploring or delegating. If the service classifies this interaction as touch
+ * exploration or delegating the framework will stop sending motion events to the service for the
+ * duration of this interaction. If the service classifies this interaction as a dragging
+ * interaction the framework will send motion events to the service to allow the service to
+ * determine if the interaction still qualifies as dragging or if it has become a delegating
+ * interaction. If {@link AccessibilityServiceInfo#FLAG_REQUEST_TOUCH_EXPLORATION_MODE } is disabled
+ * this controller will not receive any motion events because touch interactions are being passed
+ * through to the input pipeline unaltered.
+ * Note that {@link AccessibilityServiceInfo#FLAG_REQUEST_TOUCH_EXPLORATION_MODE }
+ * requires setting {@link android.R.attr#canRequestTouchExplorationMode} as well.
+ */
+public final class TouchInteractionController {
+    /** The state where the user is not touching the screen. */
+    public static final int STATE_CLEAR = 0;
+    /**
+     * The state where the user is touching the screen and the service is receiving motion events.
+     */
+    public static final int STATE_TOUCH_INTERACTING = 1;
+    /**
+     * The state where the user is explicitly exploring the screen. The service is not receiving
+     * motion events.
+     */
+    public static final int STATE_TOUCH_EXPLORING = 2;
+    /**
+     * The state where the user is dragging with two fingers. The service is not receiving motion
+     * events. The selected finger is being dispatched to the rest of the input pipeline to execute
+     * the drag.
+     */
+    public static final int STATE_DRAGGING = 3;
+    /**
+     * The user is performing a gesture which is being passed through to the input pipeline as-is.
+     * The service is not receiving motion events.
+     */
+    public static final int STATE_DELEGATING = 4;
+
+    @IntDef({
+        STATE_CLEAR,
+        STATE_TOUCH_INTERACTING,
+        STATE_TOUCH_EXPLORING,
+        STATE_DRAGGING,
+        STATE_DELEGATING
+    })
+    private @interface State {}
+
+    // The maximum number of pointers that can be touching the screen at once. (See MAX_POINTER_ID
+    // in frameworks/native/include/input/Input.h)
+    private static final int MAX_POINTER_COUNT = 32;
+
+    private final AccessibilityService mService;
+    private final Object mLock;
+    private final int mDisplayId;
+    private boolean mServiceDetectsGestures;
+    /** Map of listeners to executors. Lazily created when adding the first listener. */
+    private ArrayMap<Listener, Executor> mListeners;
+
+    // The current state of the display.
+    private int mState = STATE_CLEAR;
+
+    TouchInteractionController(
+            @NonNull AccessibilityService service, @NonNull Object lock, int displayId) {
+        mDisplayId = displayId;
+        mLock = lock;
+        mService = service;
+    }
+
+    /**
+     * Adds the specified change listener to the list of motion event listeners. The callback will
+     * run using on the specified {@link Executor}', or on the service's main thread if the
+     * Executor is {@code null}.
+     * @param listener the listener to add, must be non-null
+     * @param executor the executor for this callback, or {@code null} to execute on the service's
+     *     main thread
+     */
+    public void addListener(@Nullable Executor executor, @NonNull Listener listener) {
+        synchronized (mLock) {
+            if (mListeners == null) {
+                mListeners = new ArrayMap<>();
+            }
+            mListeners.put(listener, executor);
+            if (mListeners.size() == 1) {
+                setServiceDetectsGestures(true);
+            }
+        }
+    }
+
+    /**
+     * Removes the specified listener from the list of motion event listeners.
+     *
+     * @param listener the listener to remove, must be non-null
+     * @return {@code true} if the listener was removed, {@code false} otherwise
+     */
+    public boolean removeListener(@NonNull Listener listener) {
+        if (mListeners == null) {
+            return false;
+        }
+        synchronized (mLock) {
+            boolean result = mListeners.remove(listener) != null;
+            if (result && mListeners.size() == 0) {
+                setServiceDetectsGestures(false);
+            }
+            return result;
+        }
+    }
+
+    /**
+     * Removes all listeners and returns control of touch interactions to the framework.
+     */
+    public void removeAllListeners() {
+        if (mListeners != null) {
+            synchronized (mLock) {
+                mListeners.clear();
+                setServiceDetectsGestures(false);
+            }
+        }
+    }
+
+    /**
+     * Dispatches motion events to any registered listeners. This should be called on the service's
+     * main thread.
+     */
+    void onMotionEvent(MotionEvent event) {
+        final ArrayMap<Listener, Executor> entries;
+        synchronized (mLock) {
+            // Listeners may remove themselves. Perform a shallow copy to avoid concurrent
+            // modification.
+            entries = new ArrayMap<>(mListeners);
+        }
+        for (int i = 0, count = entries.size(); i < count; i++) {
+            final Listener listener = entries.keyAt(i);
+            final Executor executor = entries.valueAt(i);
+            if (executor != null) {
+                executor.execute(() -> listener.onMotionEvent(event));
+            } else {
+                // We're already on the main thread, just run the listener.
+                listener.onMotionEvent(event);
+            }
+        }
+    }
+
+    /**
+     * Dispatches motion events to any registered listeners. This should be called on the service's
+     * main thread.
+     */
+    void onStateChanged(@State int state) {
+        mState = state;
+        final ArrayMap<Listener, Executor> entries;
+        synchronized (mLock) {
+            // Listeners may remove themselves. Perform a shallow copy to avoid concurrent
+            // modification.
+            entries = new ArrayMap<>(mListeners);
+        }
+        for (int i = 0, count = entries.size(); i < count; i++) {
+            final Listener listener = entries.keyAt(i);
+            final Executor executor = entries.valueAt(i);
+            if (executor != null) {
+                executor.execute(() -> listener.onStateChanged(state));
+            } else {
+                // We're already on the main thread, just run the listener.
+                listener.onStateChanged(state);
+            }
+        }
+    }
+
+    /**
+     * When {@link AccessibilityServiceInfo#FLAG_REQUEST_TOUCH_EXPLORATION_MODE} is enabled, this
+     * controller will receive all motion events received by the framework for the specified display
+     * when not touch-exploring, delegating, or dragging. This allows the service to detect its own
+     * gestures, and use its own logic to judge when the framework should start touch-exploring,
+     * delegating, or dragging. If {@link
+     * AccessibilityServiceInfo#FLAG_REQUEST_TOUCH_EXPLORATION_MODE } is disabled this flag has no
+     * effect.
+     *
+     * @see AccessibilityServiceInfo#FLAG_REQUEST_TOUCH_EXPLORATION_MODE
+     */
+    private void setServiceDetectsGestures(boolean mode) {
+        final IAccessibilityServiceConnection connection =
+                AccessibilityInteractionClient.getInstance()
+                        .getConnection(mService.getConnectionId());
+        if (connection != null) {
+            try {
+                connection.setServiceDetectsGesturesEnabled(mDisplayId, mode);
+                mServiceDetectsGestures = mode;
+            } catch (RemoteException re) {
+                throw new RuntimeException(re);
+            }
+        }
+    }
+
+    /**
+     * If {@link AccessibilityServiceInfo#FLAG_REQUEST_TOUCH_EXPLORATION_MODE} is enabled and at
+     * least one listener has been added for this display this function tells the framework to
+     * initiate touch exploration. Touch exploration will continue for the duration of this
+     * interaction.
+     */
+    public void requestTouchExploration() {
+        checkState();
+        final IAccessibilityServiceConnection connection =
+                AccessibilityInteractionClient.getInstance()
+                        .getConnection(mService.getConnectionId());
+        if (connection != null) {
+            try {
+                connection.requestTouchExploration(mDisplayId);
+            } catch (RemoteException re) {
+                throw new RuntimeException(re);
+            }
+        }
+    }
+
+    /**
+     * If {@link AccessibilityServiceInfo#FLAG_REQUEST_TOUCH_EXPLORATION_MODE} and {@link If
+     * {@link AccessibilityServiceInfo#FLAG_REQUEST_TOUCH_EXPLORATION_MODE} is enabled and at least
+     * one listener has been added, this function tells the framework to initiate a dragging
+     * interaction using the specified pointer. The pointer's movements will be passed through to
+     * the rest of the input pipeline. Dragging is often used to perform two-finger scrolling.
+     *
+     * @param pointerId the pointer to be passed through to the rest of the input pipeline. If the
+     *            pointer id is valid but not actually present on the screen it will be ignored.
+     * @throws IllegalArgumentException if the pointer id is outside of the allowed range.
+     */
+    public void requestDragging(int pointerId) {
+        checkState();
+        if (pointerId < 0 || pointerId > MAX_POINTER_COUNT) {
+            throw new IllegalArgumentException("Invalid pointer id: " + pointerId);
+        }
+        final IAccessibilityServiceConnection connection =
+                AccessibilityInteractionClient.getInstance()
+                        .getConnection(mService.getConnectionId());
+        if (connection != null) {
+            try {
+                connection.requestDragging(mDisplayId, pointerId);
+            } catch (RemoteException re) {
+                throw new RuntimeException(re);
+            }
+        }
+    }
+
+    /**
+     * If {@link AccessibilityServiceInfo#FLAG_REQUEST_TOUCH_EXPLORATION_MODE} and {@link If
+     * {@link AccessibilityServiceInfo#FLAG_REQUEST_TOUCH_EXPLORATION_MODE} is enabled and at least
+     * one listener has been added, this function tells the framework to initiate a delegating
+     * interaction. Motion events will be passed through as-is to the rest of the input pipeline for
+     * the duration of this interaction.
+     */
+    public void requestDelegating() {
+        checkState();
+        final IAccessibilityServiceConnection connection =
+                AccessibilityInteractionClient.getInstance()
+                        .getConnection(mService.getConnectionId());
+        if (connection != null) {
+            try {
+                connection.requestDelegating(mDisplayId);
+            } catch (RemoteException re) {
+                throw new RuntimeException(re);
+            }
+        }
+    }
+
+    /**
+     * If {@link AccessibilityServiceInfo#FLAG_REQUEST_TOUCH_EXPLORATION_MODE} and {@link If
+     * {@link AccessibilityServiceInfo#FLAG_REQUEST_TOUCH_EXPLORATION_MODE} is enabled and at least
+     * one listener has been added, this function tells the framework to perform a click.
+     * The framework will first try to perform
+     * {@link AccessibilityNodeInfo.AccessibilityAction#ACTION_CLICK} on the item with
+     * accessibility focus. If that fails, the framework will simulate a click using motion events
+     * on the last location to have accessibility focus.
+     */
+    public void performClick() {
+        final IAccessibilityServiceConnection connection =
+                AccessibilityInteractionClient.getInstance()
+                        .getConnection(mService.getConnectionId());
+        if (connection != null) {
+            try {
+                connection.onDoubleTap(mDisplayId);
+            } catch (RemoteException re) {
+                throw new RuntimeException(re);
+            }
+        }
+    }
+
+    /**
+     * If {@link AccessibilityServiceInfo#FLAG_REQUEST_TOUCH_EXPLORATION_MODE} and {@link If
+     * {@link AccessibilityServiceInfo#FLAG_REQUEST_TOUCH_EXPLORATION_MODE} is enabled and at least
+     * one listener has been added, this function tells the framework to perform a long click.
+     * The framework will simulate a long click using motion events on the last location with
+     * accessibility focus and will delegate any movements to the rest of the input pipeline. This
+     * allows a user to double-tap and hold to trigger a drag and then execute that drag by moving
+     * their finger.
+     */
+    public void performLongClickAndStartDrag() {
+        final IAccessibilityServiceConnection connection =
+                AccessibilityInteractionClient.getInstance()
+                        .getConnection(mService.getConnectionId());
+        if (connection != null) {
+            try {
+                connection.onDoubleTapAndHold(mDisplayId);
+            } catch (RemoteException re) {
+                throw new RuntimeException(re);
+            }
+        }
+    }
+
+    private void checkState() {
+        if (!mServiceDetectsGestures || mListeners.size() == 0) {
+            throw new IllegalStateException(
+                    "State transitions are not allowed without first adding a listener.");
+        }
+        if (mState != STATE_TOUCH_INTERACTING) {
+            throw new IllegalStateException(
+                    "State transitions are not allowed in " + stateToString(mState));
+        }
+    }
+
+    /** @return the maximum number of pointers that this display will accept. */
+    public int getMaxPointerCount() {
+        return MAX_POINTER_COUNT;
+    }
+
+    /** @return the display id associated with this controller. */
+    public int getDisplayId() {
+        return mDisplayId;
+    }
+
+    /**
+     * @return the current state of this controller.
+     * @see TouchInteractionController#STATE_CLEAR
+     * @see TouchInteractionController#STATE_DELEGATING
+     * @see TouchInteractionController#STATE_DRAGGING
+     * @see TouchInteractionController#STATE_TOUCH_EXPLORING
+     */
+    public int getState() {
+        synchronized (mLock) {
+            return mState;
+        }
+    }
+
+    /** Returns a string representation of the specified state. */
+    @NonNull
+    public static String stateToString(int state) {
+        switch (state) {
+            case STATE_CLEAR:
+                return "STATE_CLEAR";
+            case STATE_TOUCH_INTERACTING:
+                return "STATE_TOUCH_INTERACTING";
+            case STATE_TOUCH_EXPLORING:
+                return "STATE_TOUCH_EXPLORING";
+            case STATE_DRAGGING:
+                return "STATE_DRAGGING";
+            case STATE_DELEGATING:
+                return "STATE_DELEGATING";
+            default:
+                return "Unknown state: " + state;
+        }
+    }
+
+    /** Listeners allow services to receive motion events and state change updates. */
+    public interface Listener {
+        /**
+         * Called when the framework has sent a motion event to the service.
+         *
+         * @param event the event being passed to the service.
+         */
+        void onMotionEvent(@NonNull MotionEvent event);
+
+        /**
+         * Called when the state of motion event dispatch for this display has changed.
+         *
+         * @param state the new state of motion event dispatch.
+         * @see TouchInteractionController#STATE_CLEAR
+         * @see TouchInteractionController#STATE_DELEGATING
+         * @see TouchInteractionController#STATE_DRAGGING
+         * @see TouchInteractionController#STATE_TOUCH_EXPLORING
+         */
+        void onStateChanged(@State int state);
+    }
+}
diff --git a/core/java/android/app/UiAutomation.java b/core/java/android/app/UiAutomation.java
index 65f71d0..828b171 100644
--- a/core/java/android/app/UiAutomation.java
+++ b/core/java/android/app/UiAutomation.java
@@ -49,6 +49,7 @@
 import android.view.Display;
 import android.view.InputEvent;
 import android.view.KeyEvent;
+import android.view.MotionEvent;
 import android.view.Surface;
 import android.view.SurfaceControl;
 import android.view.View;
@@ -1542,6 +1543,15 @@
                     return false;
                 }
 
+                public void onMotionEvent(MotionEvent event) {
+                    /* do nothing */
+                }
+
+                @Override
+                public void onTouchStateChanged(int displayId, int state) {
+                    /* do nothing */
+                }
+
                 @Override
                 public void onAccessibilityEvent(AccessibilityEvent event) {
                     final OnAccessibilityEventListener listener;
diff --git a/core/tests/coretests/src/android/view/accessibility/AccessibilityServiceConnectionImpl.java b/core/tests/coretests/src/android/view/accessibility/AccessibilityServiceConnectionImpl.java
index 3045d7d5..c241e36 100644
--- a/core/tests/coretests/src/android/view/accessibility/AccessibilityServiceConnectionImpl.java
+++ b/core/tests/coretests/src/android/view/accessibility/AccessibilityServiceConnectionImpl.java
@@ -160,15 +160,27 @@
 
     public void takeScreenshot(int displayId, RemoteCallback callback) {}
 
-    public void setTouchExplorationPassthroughRegion(int displayId, Region region) {}
-
-    public void setGestureDetectionPassthroughRegion(int displayId, Region region) {}
-
     public void setFocusAppearance(int strokeWidth, int color) {}
 
     public void logTrace(long timestamp, String where, String callingParams, int processId,
             long threadId, int callingUid, Bundle callingStack) {}
 
+    public void setGestureDetectionPassthroughRegion(int displayId, Region region) {}
+
+    public void setTouchExplorationPassthroughRegion(int displayId, Region region) {}
+
+    public void setServiceDetectsGesturesEnabled(int displayId, boolean mode) {}
+
+    public void requestTouchExploration(int displayId) {}
+
+    public void requestDragging(int displayId, int pointerId) {}
+
+    public void requestDelegating(int displayId) {}
+
+    public void onDoubleTap(int displayId) {}
+
+    public void onDoubleTapAndHold(int displayId) {}
+
     public void logTrace(long timestamp, String where, long loggingTypes, String callingParams,
             int processId, long threadId, int callingUid, Bundle serializedCallingStackInBundle) {}
 }
diff --git a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
index acdbcb58..67bb726 100644
--- a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
+++ b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
@@ -256,6 +256,18 @@
         void setGestureDetectionPassthroughRegion(int displayId, Region region);
 
         void setTouchExplorationPassthroughRegion(int displayId, Region region);
+
+        void setServiceDetectsGesturesEnabled(int displayId, boolean mode);
+
+        void requestTouchExploration(int displayId);
+
+        void requestDragging(int displayId, int pointerId);
+
+        void requestDelegating(int displayId);
+
+        void onDoubleTap(int displayId);
+
+        void onDoubleTapAndHold(int displayId);
     }
 
     public AbstractAccessibilityServiceConnection(Context context, ComponentName componentName,
@@ -1650,7 +1662,7 @@
         }
     }
 
-    private IAccessibilityServiceClient getServiceInterfaceSafely() {
+    protected IAccessibilityServiceClient getServiceInterfaceSafely() {
         synchronized (mLock) {
             return mServiceInterface;
         }
@@ -2048,4 +2060,28 @@
     protected void logTraceWM(String methodName, String params) {
         mTrace.logTrace(TRACE_WM + "." + methodName, FLAGS_WINDOW_MANAGER_INTERNAL, params);
     }
+
+    public void setServiceDetectsGesturesEnabled(int displayId, boolean mode) {
+        mSystemSupport.setServiceDetectsGesturesEnabled(displayId, mode);
+    }
+
+    public void requestTouchExploration(int displayId) {
+        mSystemSupport.requestTouchExploration(displayId);
+    }
+
+    public void requestDragging(int displayId, int pointerId) {
+        mSystemSupport.requestDragging(displayId, pointerId);
+    }
+
+    public void requestDelegating(int displayId) {
+        mSystemSupport.requestDelegating(displayId);
+    }
+
+    public void onDoubleTap(int displayId) {
+        mSystemSupport.onDoubleTap(displayId);
+    }
+
+    public void onDoubleTapAndHold(int displayId) {
+        mSystemSupport.onDoubleTapAndHold(displayId);
+    }
 }
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java
index 10cfd04..75724bf 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java
@@ -893,6 +893,42 @@
         }
     }
 
+    public void setServiceDetectsGesturesEnabled(int displayId, boolean mode) {
+        if (mTouchExplorer.contains(displayId)) {
+            mTouchExplorer.get(displayId).setServiceDetectsGestures(mode);
+        }
+    }
+
+    public void requestTouchExploration(int displayId) {
+        if (mTouchExplorer.contains(displayId)) {
+            mTouchExplorer.get(displayId).requestTouchExploration();
+        }
+    }
+
+    public void requestDragging(int displayId, int pointerId) {
+        if (mTouchExplorer.contains(displayId)) {
+            mTouchExplorer.get(displayId).requestDragging(pointerId);
+        }
+    }
+
+    public void requestDelegating(int displayId) {
+        if (mTouchExplorer.contains(displayId)) {
+            mTouchExplorer.get(displayId).requestDelegating();
+        }
+    }
+
+    public void onDoubleTap(int displayId) {
+        if (mTouchExplorer.contains(displayId)) {
+            mTouchExplorer.get(displayId).onDoubleTap();
+        }
+    }
+
+    public void onDoubleTapAndHold(int displayId) {
+        if (mTouchExplorer.contains(displayId)) {
+            mTouchExplorer.get(displayId).onDoubleTapAndHold();
+        }
+    }
+
     /**
      * Dumps all {@link AccessibilityInputFilter}s here.
      */
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index e6f91a25f..214769b 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -44,6 +44,7 @@
 import android.accessibilityservice.AccessibilityServiceInfo;
 import android.accessibilityservice.AccessibilityShortcutInfo;
 import android.accessibilityservice.IAccessibilityServiceClient;
+import android.accessibilityservice.TouchInteractionController;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.ActivityOptions;
@@ -101,6 +102,7 @@
 import android.view.IWindow;
 import android.view.KeyEvent;
 import android.view.MagnificationSpec;
+import android.view.MotionEvent;
 import android.view.WindowManager;
 import android.view.accessibility.AccessibilityEvent;
 import android.view.accessibility.AccessibilityInteractionClient;
@@ -1198,7 +1200,7 @@
     }
 
     /**
-     * Called when a gesture is detected on a display.
+     * Called when a gesture is detected on a display by the framework.
      *
      * @param gestureEvent the detail of the gesture.
      * @return true if the event is handled.
@@ -1213,6 +1215,29 @@
         }
     }
 
+    /** Send a motion event to the service to allow it to perform gesture detection. */
+    public boolean sendMotionEventToListeningServices(MotionEvent event) {
+        synchronized (mLock) {
+            if (DEBUG) {
+                Slog.d(LOG_TAG, "Sending event to service: " + event);
+            }
+            return notifyMotionEvent(event);
+        }
+    }
+
+    /**
+     * Notifies services that the touch state on a given display has changed.
+     */
+    public boolean onTouchStateChanged(int displayId, int state) {
+        synchronized (mLock) {
+            if (DEBUG) {
+                Slog.d(LOG_TAG, "Notifying touch state:"
+                        + TouchInteractionController.stateToString(state));
+            }
+            return notifyTouchState(displayId, state);
+        }
+    }
+
     /**
      * Called when the system action list is changed.
      */
@@ -1527,6 +1552,30 @@
         return false;
     }
 
+    private boolean notifyMotionEvent(MotionEvent event) {
+        AccessibilityUserState state = getCurrentUserStateLocked();
+        for (int i = state.mBoundServices.size() - 1; i >= 0; i--) {
+            AccessibilityServiceConnection service = state.mBoundServices.get(i);
+            if (service.mRequestTouchExplorationMode) {
+                service.notifyMotionEvent(event);
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private boolean notifyTouchState(int displayId, int touchState) {
+        AccessibilityUserState state = getCurrentUserStateLocked();
+        for (int i = state.mBoundServices.size() - 1; i >= 0; i--) {
+            AccessibilityServiceConnection service = state.mBoundServices.get(i);
+            if (service.mRequestTouchExplorationMode) {
+                service.notifyTouchState(displayId, touchState);
+                return true;
+            }
+        }
+        return false;
+    }
+
     private void notifyClearAccessibilityCacheLocked() {
         AccessibilityUserState state = getCurrentUserStateLocked();
         for (int i = state.mBoundServices.size() - 1; i >= 0; i--) {
@@ -2083,7 +2132,6 @@
             if (userState.isSendMotionEventsEnabled()) {
                 flags |= AccessibilityInputFilter.FLAG_SEND_MOTION_EVENTS;
             }
-
             if (userState.isAutoclickEnabledLocked()) {
                 flags |= AccessibilityInputFilter.FLAG_FEATURE_AUTOCLICK;
             }
@@ -3960,6 +4008,93 @@
         }
     }
 
+    @Override
+    public void setServiceDetectsGesturesEnabled(int displayId, boolean mode) {
+        mMainHandler.sendMessage(
+                obtainMessage(AccessibilityManagerService::setServiceDetectsGesturesInternal, this,
+                        displayId, mode));
+    }
+
+    private void setServiceDetectsGesturesInternal(int displayId, boolean mode) {
+        synchronized (mLock) {
+            if (mHasInputFilter && mInputFilter != null) {
+                mInputFilter.setServiceDetectsGesturesEnabled(displayId, mode);
+            }
+        }
+    }
+
+    @Override
+    public void requestTouchExploration(int displayId) {
+        mMainHandler.sendMessage(obtainMessage(
+                AccessibilityManagerService::requestTouchExplorationInternal, this, displayId));
+    }
+
+    private void requestTouchExplorationInternal(int displayId) {
+        synchronized (mLock) {
+            if (mHasInputFilter && mInputFilter != null) {
+                mInputFilter.requestTouchExploration(displayId);
+            }
+        }
+    }
+
+    @Override
+    public void requestDragging(int displayId, int pointerId) {
+        mMainHandler.sendMessage(obtainMessage(AccessibilityManagerService::requestDraggingInternal,
+                this, displayId, pointerId));
+    }
+
+    private void requestDraggingInternal(int displayId, int pointerId) {
+        synchronized (mLock) {
+            if (mHasInputFilter && mInputFilter != null) {
+                mInputFilter.requestDragging(displayId, pointerId);
+            }
+        }
+    }
+
+    @Override
+    public void requestDelegating(int displayId) {
+        mMainHandler.sendMessage(
+                obtainMessage(
+                        AccessibilityManagerService::requestDelegatingInternal, this, displayId));
+    }
+
+    private void requestDelegatingInternal(int displayId) {
+        synchronized (mLock) {
+            if (mHasInputFilter && mInputFilter != null) {
+                mInputFilter.requestDelegating(displayId);
+            }
+        }
+    }
+
+    @Override
+    public void onDoubleTap(int displayId) {
+        mMainHandler.sendMessage(obtainMessage(AccessibilityManagerService::onDoubleTapInternal,
+                this, displayId));
+    }
+
+    private void onDoubleTapInternal(int displayId) {
+        synchronized (mLock) {
+            if (mHasInputFilter && mInputFilter != null) {
+                mInputFilter.onDoubleTap(displayId);
+            }
+        }
+    }
+
+    @Override
+    public void onDoubleTapAndHold(int displayId) {
+        mMainHandler
+                .sendMessage(obtainMessage(AccessibilityManagerService::onDoubleTapAndHoldInternal,
+                        this, displayId));
+    }
+
+    private void onDoubleTapAndHoldInternal(int displayId) {
+        synchronized (mLock) {
+            if (mHasInputFilter && mInputFilter != null) {
+                mInputFilter.onDoubleTapAndHold(displayId);
+            }
+        }
+    }
+
     private void updateFocusAppearanceDataLocked(AccessibilityUserState userState) {
         if (userState.mUserId != mCurrentUserId) {
             return;
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java
index 4bf48a2..e9f5870 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java
@@ -22,6 +22,7 @@
 import android.accessibilityservice.AccessibilityServiceInfo;
 import android.accessibilityservice.AccessibilityTrace;
 import android.accessibilityservice.IAccessibilityServiceClient;
+import android.accessibilityservice.TouchInteractionController;
 import android.app.PendingIntent;
 import android.content.ComponentName;
 import android.content.Context;
@@ -30,12 +31,15 @@
 import android.os.Binder;
 import android.os.Handler;
 import android.os.IBinder;
+import android.os.Message;
 import android.os.Process;
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.provider.Settings;
 import android.util.Slog;
 import android.view.Display;
+import android.view.MotionEvent;
+
 
 import com.android.server.inputmethod.InputMethodManagerInternal;
 import com.android.server.wm.ActivityTaskManagerInternal;
@@ -454,4 +458,48 @@
             mSystemSupport.onClientChangeLocked(false);
         }
     }
+
+    public void notifyMotionEvent(MotionEvent event) {
+        final Message msg = obtainMessage(
+                AccessibilityServiceConnection::notifyMotionEventInternal,
+                AccessibilityServiceConnection.this, event);
+        mMainHandler.sendMessage(msg);
+    }
+
+    public void notifyTouchState(int displayId, int state) {
+        final Message msg = obtainMessage(
+                AccessibilityServiceConnection::notifyTouchStateInternal,
+                AccessibilityServiceConnection.this, displayId, state);
+        mMainHandler.sendMessage(msg);
+    }
+
+    private void notifyMotionEventInternal(MotionEvent event) {
+        final IAccessibilityServiceClient listener = getServiceInterfaceSafely();
+        if (listener != null) {
+            try {
+                if (mTrace.isA11yTracingEnabled()) {
+                    logTraceSvcClient(".onMotionEvent ",
+                            event.toString());
+                }
+                listener.onMotionEvent(event);
+            } catch (RemoteException re) {
+                Slog.e(LOG_TAG, "Error sending motion event to" + mService, re);
+            }
+        }
+    }
+
+    private void notifyTouchStateInternal(int displayId, int state) {
+        final IAccessibilityServiceClient listener = getServiceInterfaceSafely();
+        if (listener != null) {
+            try {
+                if (mTrace.isA11yTracingEnabled()) {
+                    logTraceSvcClient(".onTouchStateChanged ",
+                            TouchInteractionController.stateToString(state));
+                }
+                listener.onTouchStateChanged(displayId, state);
+            } catch (RemoteException re) {
+                Slog.e(LOG_TAG, "Error sending motion event to" + mService, re);
+            }
+        }
+    }
 }
diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/EventDispatcher.java b/services/accessibility/java/com/android/server/accessibility/gestures/EventDispatcher.java
index df4a52e..b6223c7 100644
--- a/services/accessibility/java/com/android/server/accessibility/gestures/EventDispatcher.java
+++ b/services/accessibility/java/com/android/server/accessibility/gestures/EventDispatcher.java
@@ -382,18 +382,21 @@
     }
 
     public boolean longPressWithTouchEvents(MotionEvent event, int policyFlags) {
-        final int pointerIndex = event.getActionIndex();
-        final int pointerId = event.getPointerId(pointerIndex);
         Point clickLocation = mTempPoint;
         final int result = computeClickLocation(clickLocation);
         if (result == CLICK_LOCATION_NONE) {
             return false;
         }
-        mLongPressingPointerId = pointerId;
-        mLongPressingPointerDeltaX = (int) event.getX(pointerIndex) - clickLocation.x;
-        mLongPressingPointerDeltaY = (int) event.getY(pointerIndex) - clickLocation.y;
-        sendDownForAllNotInjectedPointers(event, policyFlags);
-        return true;
+        if (event != null) {
+            final int pointerIndex = event.getActionIndex();
+            final int pointerId = event.getPointerId(pointerIndex);
+            mLongPressingPointerId = pointerId;
+            mLongPressingPointerDeltaX = (int) event.getX(pointerIndex) - clickLocation.x;
+            mLongPressingPointerDeltaY = (int) event.getY(pointerIndex) - clickLocation.y;
+            sendDownForAllNotInjectedPointers(event, policyFlags);
+            return true;
+        }
+        return false;
     }
 
     void clear() {
diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java b/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java
index 74f0bcb..946d22e4 100644
--- a/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java
+++ b/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java
@@ -46,6 +46,7 @@
 import android.os.Handler;
 import android.util.DisplayMetrics;
 import android.util.Slog;
+import android.view.Display;
 import android.view.InputDevice;
 import android.view.MotionEvent;
 import android.view.ViewConfiguration;
@@ -152,6 +153,7 @@
 
     private Region mGestureDetectionPassthroughRegion;
     private Region mTouchExplorationPassthroughRegion;
+    private int mDisplayId = Display.INVALID_DISPLAY;
 
 /**
      * Creates a new instance.
@@ -182,8 +184,9 @@
     TouchExplorer(Context context, AccessibilityManagerService service, GestureManifold detector,
             @NonNull Handler mainHandler) {
         mContext = context;
+        mDisplayId = context.getDisplayId();
         mAms = service;
-        mState = new TouchState();
+        mState = new TouchState(mDisplayId, mAms);
         mReceivedPointerTracker = mState.getReceivedPointerTracker();
         mDispatcher = new EventDispatcher(context, mAms, super.getNext(), mState);
         mDetermineUserIntentTimeout = ViewConfiguration.getDoubleTapTimeout();
@@ -267,6 +270,7 @@
         }
         try {
             checkForMalformedEvent(event);
+            checkForMalformedEvent(rawEvent);
         } catch (IllegalArgumentException e) {
             Slog.e(LOG_TAG, "Ignoring malformed event: " + event.toString(), e);
             return;
@@ -277,7 +281,7 @@
             Slog.d(LOG_TAG, mState.toString());
         }
 
-        mState.onReceivedMotionEvent(rawEvent);
+        mState.onReceivedMotionEvent(event, rawEvent, policyFlags);
         if (shouldPerformGestureDetection(event)) {
             if (mGestureDetector.onMotionEvent(event, rawEvent, policyFlags)) {
                 // Event was handled by the gesture detector.
@@ -307,9 +311,12 @@
             // It will be delivered on gesture completion or cancelation.
             // Note that the delay for sending GESTURE_DETECTION_END remains in place.
             mSendTouchInteractionEndDelayed.cancel();
+            if (mState.isServiceDetectingGestures()) {
+                mAms.sendMotionEventToListeningServices(rawEvent);
+            }
         } else {
             Slog.e(LOG_TAG, "Illegal state: " + mState);
-                clear(event, policyFlags);
+            clear(event, policyFlags);
         }
     }
 
@@ -367,7 +374,7 @@
                 AccessibilityGestureEvent gestureEvent =
                         new AccessibilityGestureEvent(
                                 AccessibilityService.GESTURE_DOUBLE_TAP_AND_HOLD,
-                                event.getDisplayId(),
+                                mDisplayId,
                                 mGestureDetector.getMotionEvents());
                 dispatchGesture(gestureEvent);
             }
@@ -389,7 +396,7 @@
             AccessibilityGestureEvent gestureEvent =
                     new AccessibilityGestureEvent(
                             AccessibilityService.GESTURE_DOUBLE_TAP,
-                            event.getDisplayId(),
+                            mDisplayId,
                             mGestureDetector.getMotionEvents());
             dispatchGesture(gestureEvent);
         }
@@ -404,13 +411,37 @@
         if (!mAms.performActionOnAccessibilityFocusedItem(
                 AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK)) {
             Slog.e(LOG_TAG, "ACTION_CLICK failed. Dispatching motion events to simulate click.");
-
-            mDispatcher.clickWithTouchEvents(event, rawEvent, policyFlags);
+            if (event != null && rawEvent != null) {
+                mDispatcher.clickWithTouchEvents(event, rawEvent, policyFlags);
+            }
             return true;
         }
         return true;
     }
 
+    /**
+     * Executes a double-tap. The framework will first attempt to execute the appropriate
+     * accessibility action and if that fails, the framework will deliver touch events to the last
+     * touch-explored location.
+     */
+    public void onDoubleTap() {
+        MotionEvent event = mState.getLastReceivedEvent();
+        MotionEvent rawEvent = mState.getLastReceivedRawEvent();
+        int policyFlags = mState.getLastReceivedPolicyFlags();
+        onDoubleTap(event, rawEvent, policyFlags);
+    }
+
+    /**
+     * Executes a double-tap and hold gesture using touch events. The user can continue to move
+     * their finger around the screen to execute a drag.
+     */
+    public void onDoubleTapAndHold() {
+        MotionEvent event = mState.getLastReceivedEvent();
+        MotionEvent rawEvent = mState.getLastReceivedRawEvent();
+        int policyFlags = mState.getLastReceivedPolicyFlags();
+        onDoubleTapAndHold(event, rawEvent, policyFlags);
+    }
+
     @Override
     public boolean onGestureStarted() {
         if (mAms.getTraceManager().isA11yTracingEnabledForTypes(LOGGING_FLAGS)) {
@@ -473,7 +504,7 @@
             AccessibilityGestureEvent gestureEvent =
                     new AccessibilityGestureEvent(
                             AccessibilityService.GESTURE_UNKNOWN,
-                            event.getDisplayId(),
+                            mDisplayId,
                             mGestureDetector.getMotionEvents());
             dispatchGesture(gestureEvent);
         }
@@ -502,7 +533,6 @@
      */
     private void handleActionDown(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
         mAms.onTouchInteractionStart();
-
         // If we still have not notified the user for the last
         // touch, we figure out what to do. If were waiting
         // we resent the delayed callback and wait again.
@@ -512,7 +542,6 @@
         if (mState.isTouchExploring()) {
             sendHoverExitAndTouchExplorationGestureEndIfNeeded(policyFlags);
         }
-
         if (mState.isClear()) {
             if (!mSendHoverEnterAndMoveDelayed.isPending()) {
                 // Queue a delayed transition to STATE_TOUCH_EXPLORING.
@@ -521,7 +550,14 @@
                 // The idea is to avoid getting stuck in STATE_TOUCH_INTERACTING
                 final int pointerId = mReceivedPointerTracker.getPrimaryPointerId();
                 final int pointerIdBits = (1 << pointerId);
-                mSendHoverEnterAndMoveDelayed.post(event, rawEvent, pointerIdBits, policyFlags);
+                if (mState.isServiceDetectingGestures()) {
+                    // This transition will be triggered manually by the service.
+                    mSendHoverEnterAndMoveDelayed.setPointerIdBits(pointerIdBits);
+                    mSendHoverEnterAndMoveDelayed.setPolicyFlags(policyFlags);
+                    mSendHoverEnterAndMoveDelayed.addEvent(event, rawEvent);
+                } else {
+                    mSendHoverEnterAndMoveDelayed.post(event, rawEvent, pointerIdBits, policyFlags);
+                }
             } else {
                 // Cache the event until we discern exploration from gesturing.
                 mSendHoverEnterAndMoveDelayed.addEvent(event, rawEvent);
@@ -548,6 +584,9 @@
             // Avoid duplicated TYPE_TOUCH_INTERACTION_START event when 2nd tap of double tap.
             mSendTouchInteractionEndDelayed.cancel();
         }
+        if (mState.isServiceDetectingGestures()) {
+            mAms.sendMotionEventToListeningServices(rawEvent);
+        }
     }
 
     /**
@@ -571,6 +610,11 @@
             case ACTION_MOVE:
                 handleActionMoveStateTouchInteracting(event, rawEvent, policyFlags);
                 break;
+            case ACTION_POINTER_UP:
+                if (mState.isServiceDetectingGestures()) {
+                    mAms.sendMotionEventToListeningServices(rawEvent);
+                }
+                break;
             case ACTION_UP:
                 handleActionUp(event, rawEvent, policyFlags);
                 break;
@@ -612,15 +656,16 @@
         // Another finger down means that if we have not started to deliver
         // hover events, we will not have to. The code for ACTION_MOVE will
         // decide what we will actually do next.
-
         if (mSendHoverEnterAndMoveDelayed.isPending()) {
             mSendHoverEnterAndMoveDelayed.cancel();
             mSendHoverExitDelayed.cancel();
         } else {
-            // We have already delivered at least one hover event, so send hover exit to keep the
-            // stream consistent.
+            // We have already delivered at least one hover event, so send hover exit to keep
             sendHoverExitAndTouchExplorationGestureEndIfNeeded(policyFlags);
         }
+        if (mState.isServiceDetectingGestures()) {
+            mAms.sendMotionEventToListeningServices(rawEvent);
+        }
     }
 
     /**
@@ -632,12 +677,19 @@
         final int pointerId = mReceivedPointerTracker.getPrimaryPointerId();
         final int pointerIndex = event.findPointerIndex(pointerId);
         int pointerIdBits = (1 << pointerId);
+        if (mState.isServiceDetectingGestures()) {
+            mAms.sendMotionEventToListeningServices(rawEvent);
+            mSendHoverEnterAndMoveDelayed.addEvent(event, rawEvent);
+            return;
+        }
         switch (event.getPointerCount()) {
             case 1:
                 // We have not started sending events since we try to
                 // figure out what the user is doing.
                 if (mSendHoverEnterAndMoveDelayed.isPending()) {
                     // Cache the event until we discern exploration from gesturing.
+                    // When the service is detecting gestures we rely on it to fire touch
+                    // exploration.
                     mSendHoverEnterAndMoveDelayed.addEvent(event, rawEvent);
                 }
                 break;
@@ -684,7 +736,7 @@
                         AccessibilityGestureEvent gestureEvent =
                                 new AccessibilityGestureEvent(
                                         AccessibilityService.GESTURE_PASSTHROUGH,
-                                        event.getDisplayId(),
+                                        mDisplayId,
                                         mGestureDetector.getMotionEvents());
                         dispatchGesture(gestureEvent);
                     }
@@ -708,7 +760,7 @@
                         AccessibilityGestureEvent gestureEvent =
                                 new AccessibilityGestureEvent(
                                         AccessibilityService.GESTURE_PASSTHROUGH,
-                                        event.getDisplayId(),
+                                        mDisplayId,
                                         mGestureDetector.getMotionEvents());
                         dispatchGesture(gestureEvent);
                     }
@@ -730,7 +782,7 @@
                                     AccessibilityGestureEvent gestureEvent =
                                             new AccessibilityGestureEvent(
                                                     AccessibilityService.GESTURE_PASSTHROUGH,
-                                                    event.getDisplayId(),
+                                                    mDisplayId,
                                                     mGestureDetector.getMotionEvents());
                                     dispatchGesture(gestureEvent);
                                 }
@@ -751,7 +803,7 @@
                         AccessibilityGestureEvent gestureEvent =
                                 new AccessibilityGestureEvent(
                                         AccessibilityService.GESTURE_PASSTHROUGH,
-                                        event.getDisplayId(),
+                                        mDisplayId,
                                         mGestureDetector.getMotionEvents());
                         dispatchGesture(gestureEvent);
                     }
@@ -767,7 +819,10 @@
      * Handles ACTION_UP while in the touch interacting state. This event represents all fingers
      * being lifted from the screen.
      */
-    private void handleActionUp(MotionEvent event,  MotionEvent rawEvent, int policyFlags) {
+    private void handleActionUp(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
+        if (mState.isServiceDetectingGestures() && mState.isTouchInteracting()) {
+            mAms.sendMotionEventToListeningServices(rawEvent);
+        }
         mAms.onTouchInteractionEnd();
         final int pointerId = event.getPointerId(event.getActionIndex());
         final int pointerIdBits = (1 << pointerId);
@@ -883,6 +938,10 @@
                 clear(event, policyFlags);
                 return;
             case ACTION_POINTER_DOWN:
+                if (mState.isServiceDetectingGestures()) {
+                    mAms.sendMotionEventToListeningServices(rawEvent);
+                    return;
+                }
                 // We are in dragging state so we have two pointers and another one
                 // goes down => delegate the three pointers to the view hierarchy
                 mState.startDelegating();
@@ -896,6 +955,15 @@
                 if (mDraggingPointerId == INVALID_POINTER_ID) {
                     break;
                 }
+                if (mState.isServiceDetectingGestures()) {
+                    // Allow the service to judge whether this is dragging or delegation
+                    mAms.sendMotionEventToListeningServices(rawEvent);
+                    computeDraggingPointerIdIfNeeded(event);
+                    mDispatcher.sendMotionEvent(
+                            event, ACTION_MOVE, rawEvent, pointerIdBits, policyFlags);
+                    return;
+
+                }
                 switch (event.getPointerCount()) {
                     case 1:
                         // do nothing
@@ -921,6 +989,10 @@
                         }
                         break;
                     default:
+                        if (mState.isServiceDetectingGestures()) {
+                            mAms.sendMotionEventToListeningServices(rawEvent);
+                            return;
+                        }
                         mState.startDelegating();
                         mDraggingPointerId = INVALID_POINTER_ID;
                         event = MotionEvent.obtainNoHistory(event);
@@ -1210,9 +1282,7 @@
         mTouchExplorationPassthroughRegion = region;
     }
 
-    /**
-     * Whether to send the motion events that make up each gesture to the accessibility service.
-     */
+    /** Whether to send the motion events that make up each gesture to the accessibility service. */
     public void setSendMotionEventsEnabled(boolean mode) {
         mGestureDetector.setSendMotionEventsEnabled(mode);
     }
@@ -1221,7 +1291,15 @@
         return mGestureDetector.isSendMotionEventsEnabled();
     }
 
+    /** Sets whether or not motion events should be passed to the service to detect gestures. */
+    public void setServiceDetectsGestures(boolean mode) {
+        mState.setServiceDetectsGestures(mode);
+    }
+
     private boolean shouldPerformGestureDetection(MotionEvent event) {
+        if (mState.isServiceDetectingGestures()) {
+            return false;
+        }
         if (mState.isDelegating() || mState.isDragging()) {
             return false;
         }
@@ -1237,8 +1315,105 @@
     }
 
     /**
-     * Class for delayed exiting from gesture detecting mode.
+     * This method allows the service to request that TouchExplorer enter the touch exploration
+     * state.
      */
+    public void requestTouchExploration() {
+        if (DEBUG) {
+            Slog.d(LOG_TAG, "Starting touch explorer from service.");
+        }
+        if (mState.isServiceDetectingGestures() && mState.isTouchInteracting()) {
+            // Cancel without deleting events.
+            mHandler.removeCallbacks(mSendHoverEnterAndMoveDelayed);
+            mSendHoverEnterAndMoveDelayed.run();
+            mSendHoverEnterAndMoveDelayed.clear();
+            final MotionEvent prototype = mState.getLastReceivedEvent();
+            final MotionEvent rawEvent = mState.getLastReceivedRawEvent();
+            final int pointerId = mReceivedPointerTracker.getPrimaryPointerId();
+            final int pointerIdBits = (1 << pointerId);
+            final int policyFlags = mState.getLastReceivedPolicyFlags();
+            mSendHoverExitDelayed.post(prototype, rawEvent, pointerIdBits, policyFlags);
+        }
+    }
+
+    /** This method allows the service to request that TouchExplorer enter the dragging state. */
+    public void requestDragging(int pointerId) {
+        if (mState.isServiceDetectingGestures()) {
+            if (pointerId < 0 || pointerId > TouchState.MAX_POINTER_COUNT
+                    || !mReceivedPointerTracker.isReceivedPointerDown(pointerId)) {
+                Slog.e(LOG_TAG, "Trying to drag with invalid pointer: " + pointerId);
+                return;
+            }
+            if (mState.isTouchExploring()) {
+                if (mSendHoverExitDelayed.isPending()) {
+                    mSendHoverExitDelayed.forceSendAndRemove();
+                }
+                if (mSendTouchExplorationEndDelayed.isPending()) {
+                    mSendTouchExplorationEndDelayed.forceSendAndRemove();
+                }
+            }
+            if (!mState.isTouchInteracting()) {
+                // It makes no sense to drag.
+                Slog.e(LOG_TAG, "Error: Trying to drag from "
+                        + mState.getStateSymbolicName(mState.getState()));
+                return;
+            }
+            mDraggingPointerId = pointerId;
+            if (DEBUG) {
+                Slog.d(LOG_TAG, "Drag requested on pointer " + mDraggingPointerId);
+            }
+            MotionEvent event = mState.getLastReceivedEvent();
+            MotionEvent rawEvent = mState.getLastReceivedRawEvent();
+            if (event == null || rawEvent == null) {
+                Slog.e(LOG_TAG, "Unable to start dragging: unable to get last event.");
+                return;
+            }
+            int policyFlags = mState.getLastReceivedPolicyFlags();
+            int pointerIdBits = 1 << mDraggingPointerId;
+            event.setEdgeFlags(mReceivedPointerTracker.getLastReceivedDownEdgeFlags());
+            MotionEvent downEvent = computeDownEventForDrag(event);
+            mState.startDragging();
+            if (downEvent != null) {
+                mDispatcher.sendMotionEvent(
+                        downEvent, ACTION_DOWN, rawEvent, pointerIdBits, policyFlags);
+                mDispatcher.sendMotionEvent(event, ACTION_MOVE, rawEvent, pointerIdBits,
+                        policyFlags);
+            } else {
+                mDispatcher.sendMotionEvent(event, ACTION_DOWN, rawEvent, pointerIdBits,
+                        policyFlags);
+            }
+        }
+    }
+
+    /** This method allows the service to request that TouchExplorer enter the delegating state. */
+    public void requestDelegating() {
+        if (mState.isServiceDetectingGestures()) {
+            if (mState.isTouchExploring()) {
+                if (mSendHoverExitDelayed.isPending()) {
+                    mSendHoverExitDelayed.forceSendAndRemove();
+                }
+                if (mSendTouchExplorationEndDelayed.isPending()) {
+                    mSendTouchExplorationEndDelayed.forceSendAndRemove();
+                }
+            }
+            if (!mState.isTouchInteracting()) {
+                // It makes no sense to delegate.
+                Slog.e(LOG_TAG, "Error: Trying to delegate from "
+                        + mState.getStateSymbolicName(mState.getState()));
+                return;
+            }
+            mState.startDelegating();
+            MotionEvent prototype = mState.getLastReceivedEvent();
+            if (prototype == null) {
+                Slog.d(LOG_TAG, "Unable to start delegating: unable to get last received event.");
+                return;
+            }
+            int policyFlags = mState.getLastReceivedPolicyFlags();
+            mDispatcher.sendDownForAllNotInjectedPointers(prototype, policyFlags);
+        }
+    }
+
+    /** Class for delayed exiting from gesture detecting mode. */
     private final class ExitGestureDetectionModeDelayed implements Runnable {
 
         public void post() {
@@ -1283,9 +1458,7 @@
         }
     }
 
-    /**
-     * Class for delayed sending of hover enter and move events.
-     */
+    /** Class for delayed sending of hover enter and move events. */
     class SendHoverEnterAndMoveDelayed implements Runnable {
         private final String LOG_TAG_SEND_HOVER_DELAYED = "SendHoverEnterAndMoveDelayed";
 
@@ -1362,31 +1535,46 @@
             }
             if (!mEvents.isEmpty() && !mRawEvents.isEmpty()) {
                 // Deliver a down event.
-                mDispatcher.sendMotionEvent(mEvents.get(0), ACTION_HOVER_ENTER,
-                        mRawEvents.get(0), mPointerIdBits, mPolicyFlags);
+                mDispatcher.sendMotionEvent(
+                        mEvents.get(0),
+                        ACTION_HOVER_ENTER,
+                        mRawEvents.get(0),
+                        mPointerIdBits,
+                        mPolicyFlags);
                 if (DEBUG) {
-                    Slog.d(LOG_TAG_SEND_HOVER_DELAYED,
+                    Slog.d(
+                            LOG_TAG_SEND_HOVER_DELAYED,
                             "Injecting motion event: ACTION_HOVER_ENTER");
                 }
 
                 // Deliver move events.
                 final int eventCount = mEvents.size();
                 for (int i = 1; i < eventCount; i++) {
-                    mDispatcher.sendMotionEvent(mEvents.get(i), ACTION_HOVER_MOVE,
-                            mRawEvents.get(i), mPointerIdBits, mPolicyFlags);
+                    mDispatcher.sendMotionEvent(
+                            mEvents.get(i),
+                            ACTION_HOVER_MOVE,
+                            mRawEvents.get(i),
+                            mPointerIdBits,
+                            mPolicyFlags);
                     if (DEBUG) {
-                        Slog.d(LOG_TAG_SEND_HOVER_DELAYED,
+                        Slog.d(
+                                LOG_TAG_SEND_HOVER_DELAYED,
                                 "Injecting motion event: ACTION_HOVER_MOVE");
                     }
                 }
             }
             clear();
         }
-    }
 
-    /**
-     * Class for delayed sending of hover exit events.
-     */
+        public void setPointerIdBits(int pointerIdBits) {
+            mPointerIdBits = pointerIdBits;
+        }
+
+        public void setPolicyFlags(int policyFlags) {
+            mPolicyFlags = policyFlags;
+        }
+    }
+    /** Class for delayed sending of hover exit events. */
     class SendHoverExitDelayed implements Runnable {
         private final String LOG_TAG_SEND_HOVER_DELAYED = "SendHoverExitDelayed";
 
@@ -1438,21 +1626,18 @@
 
         public void run() {
             if (DEBUG) {
-                Slog.d(LOG_TAG_SEND_HOVER_DELAYED, "Injecting motion event:"
-                        + " ACTION_HOVER_EXIT");
+                Slog.d(
+                        LOG_TAG_SEND_HOVER_DELAYED,
+                        "Injecting motion event:" + " ACTION_HOVER_EXIT");
             }
             mDispatcher.sendMotionEvent(
-                    mPrototype,
-                    ACTION_HOVER_EXIT,
-                    mRawEvent,
-                    mPointerIdBits,
-                    mPolicyFlags);
+                    mPrototype, ACTION_HOVER_EXIT, mRawEvent, mPointerIdBits, mPolicyFlags);
             if (!mSendTouchExplorationEndDelayed.isPending()) {
                 mSendTouchExplorationEndDelayed.cancel();
                 mSendTouchExplorationEndDelayed.post();
             }
             if (mSendTouchInteractionEndDelayed.isPending()) {
-                  mSendTouchInteractionEndDelayed.cancel();
+                mSendTouchInteractionEndDelayed.cancel();
                 mSendTouchInteractionEndDelayed.post();
             }
             clear();
diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/TouchState.java b/services/accessibility/java/com/android/server/accessibility/gestures/TouchState.java
index 6ff0826..eb71885 100644
--- a/services/accessibility/java/com/android/server/accessibility/gestures/TouchState.java
+++ b/services/accessibility/java/com/android/server/accessibility/gestures/TouchState.java
@@ -22,9 +22,12 @@
 
 import android.annotation.IntDef;
 import android.util.Slog;
+import android.view.Display;
 import android.view.MotionEvent;
 import android.view.accessibility.AccessibilityEvent;
 
+import com.android.server.accessibility.AccessibilityManagerService;
+
 /**
  * This class describes the state of the touch explorer as well as the state of received and
  * injected pointers. This data is accessed both for purposes of touch exploration and gesture
@@ -36,7 +39,7 @@
     // This constant captures the current implementation detail that
     // pointer IDs are between 0 and 31 inclusive (subject to change).
     // (See MAX_POINTER_ID in frameworks/base/include/ui/Input.h)
-    static final int MAX_POINTER_COUNT = 32;
+    public static final int MAX_POINTER_COUNT = 32;
     // Constant referring to the ids bits of all pointers.
     public static final int ALL_POINTER_ID_BITS = 0xFFFFFFFF;
 
@@ -50,8 +53,8 @@
     public static final int STATE_TOUCH_EXPLORING = 2;
     // the user is dragging with two fingers.
     public static final int STATE_DRAGGING = 3;
-    // The user is performing some other two finger gesture which we pass through to the view
-    // hierarchy as a one-finger gesture e.g. two-finger scrolling.
+    // The user is performing some other two finger gesture which we pass through to the
+    // input pipeline as a one-finger gesture e.g. two-finger pinch.
     public static final int STATE_DELEGATING = 4;
     // The user is performing something that might be a gesture.
     public static final int STATE_GESTURE_DETECTING = 5;
@@ -75,6 +78,8 @@
     private MotionEvent mLastReceivedEvent;
     // The accompanying raw event without any transformations.
     private MotionEvent mLastReceivedRawEvent;
+    // The policy flags of the last received event.
+    int mLastReceivedPolicyFlags;
     // The id of the last touch explored window.
     private int mLastTouchedWindowId;
     // The last injected hover event.
@@ -85,14 +90,23 @@
     private long mLastInjectedDownEventTime;
     // Keep track of which pointers sent to the system are down.
     private int mInjectedPointersDown;
+    private boolean mServiceDetectsGestures = false;
+    // The requested mode for mServiceDetectsGestures. This will take effect on the next touch
+    // interaction.
+    private boolean mServiceDetectsGesturesRequested = false;
+    private AccessibilityManagerService mAms;
+    private int mDisplayId = Display.INVALID_DISPLAY;
 
-    public TouchState() {
+    public TouchState(int displayId, AccessibilityManagerService ams) {
+        mDisplayId = displayId;
+        mAms = ams;
         mReceivedPointerTracker = new ReceivedPointerTracker();
     }
 
     /** Clears the internal shared state. */
     public void clear() {
         setState(STATE_CLEAR);
+        mServiceDetectsGestures = mServiceDetectsGesturesRequested;
         // Reset the pointer trackers.
         if (mLastReceivedEvent != null) {
             mLastReceivedEvent.recycle();
@@ -107,14 +121,19 @@
      *
      * @param rawEvent The raw touch event.
      */
-    public void onReceivedMotionEvent(MotionEvent rawEvent) {
+    public void onReceivedMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
+        if (isClear() && event.getActionMasked() == MotionEvent.ACTION_DOWN) {
+            clear();
+        }
         if (mLastReceivedEvent != null) {
             mLastReceivedEvent.recycle();
         }
         if (mLastReceivedRawEvent != null) {
             mLastReceivedRawEvent.recycle();
         }
-        mLastReceivedEvent = MotionEvent.obtain(rawEvent);
+        mLastReceivedEvent = MotionEvent.obtain(event);
+        mLastReceivedRawEvent = MotionEvent.obtain(rawEvent);
+        mLastReceivedPolicyFlags = policyFlags;
         mReceivedPointerTracker.onMotionEvent(rawEvent);
     }
 
@@ -123,7 +142,7 @@
      *
      * @param event The event to process.
      */
-    void onInjectedMotionEvent(MotionEvent event) {
+    public void onInjectedMotionEvent(MotionEvent event) {
         final int action = event.getActionMasked();
         final int pointerId = event.getPointerId(event.getActionIndex());
         final int pointerFlag = (1 << pointerId);
@@ -183,6 +202,7 @@
         }
     }
 
+    /** Updates the state in response to an injected accessibility event. */
     public void onInjectedAccessibilityEvent(int type) {
         // The below state transitions go here because the related events are often sent on a
         // delay.
@@ -195,7 +215,8 @@
                 startTouchInteracting();
                 break;
             case AccessibilityEvent.TYPE_TOUCH_INTERACTION_END:
-                clear();
+                setState(STATE_CLEAR);
+                // We will clear when we actually handle the next ACTION_DOWN.
                 break;
             case AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_START:
                 startTouchExploring();
@@ -228,6 +249,9 @@
             Slog.i(LOG_TAG, getStateSymbolicName(mState) + "->" + getStateSymbolicName(state));
         }
         mState = state;
+        if (mServiceDetectsGestures) {
+            mAms.onTouchStateChanged(mDisplayId, state);
+        }
     }
 
     public boolean isTouchExploring() {
@@ -314,6 +338,16 @@
         return mLastReceivedEvent;
     }
 
+    /** Gets the most recently received policy flags. */
+    public int getLastReceivedPolicyFlags() {
+        return mLastReceivedPolicyFlags;
+    }
+
+    /** Gets the most recently received raw event. */
+    public MotionEvent getLastReceivedRawEvent() {
+        return mLastReceivedRawEvent;
+    }
+
     /** @return The the last injected hover event. */
     public MotionEvent getLastInjectedHoverEvent() {
         return mLastInjectedHoverEvent;
@@ -354,6 +388,18 @@
         return mLastInjectedHoverEventForClick;
     }
 
+    public boolean isServiceDetectingGestures() {
+        return mServiceDetectsGestures;
+    }
+
+    /** Whether the service is handling gesture detection. */
+    public void setServiceDetectsGestures(boolean mode) {
+        if (DEBUG) {
+            Slog.d(LOG_TAG, "serviceDetectsGestures: " + mode);
+        }
+        mServiceDetectsGesturesRequested = mode;
+    }
+
     /** This class tracks where and when a pointer went down. It does not track its movement. */
     class ReceivedPointerTracker {
         private static final String LOG_TAG_RECEIVED_POINTER_TRACKER = "ReceivedPointerTracker";
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/gestures/GestureManifoldTest.java b/services/tests/servicestests/src/com/android/server/accessibility/gestures/GestureManifoldTest.java
index 0e78785..45f0e67 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/gestures/GestureManifoldTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/gestures/GestureManifoldTest.java
@@ -27,18 +27,20 @@
 import android.graphics.Point;
 import android.graphics.PointF;
 import android.os.Handler;
+import android.view.Display;
 import android.view.MotionEvent;
 
 import androidx.test.InstrumentationRegistry;
 
+import com.android.server.accessibility.AccessibilityManagerService;
+
 import org.junit.Before;
 import org.junit.Test;
+import org.mockito.Mock;
 
 import java.util.ArrayList;
 
-/**
- * Tests for GestureManifold
- */
+/** Tests for GestureManifold */
 public class GestureManifoldTest {
 
     // Constants for testRecognizeGesturePath()
@@ -50,25 +52,23 @@
     private GestureManifold mManifold;
     private TouchState mState;
     private GestureManifold.Listener mResultListener;
+    @Mock private AccessibilityManagerService mMockAms;
 
     @Before
     public void setUp() {
         Context context = InstrumentationRegistry.getContext();
         // Construct a testable GestureManifold.
         mResultListener = mock(GestureManifold.Listener.class);
-        mState = new TouchState();
+        mState = new TouchState(Display.DEFAULT_DISPLAY, mMockAms);
         Handler handler = new Handler(context.getMainLooper());
         mManifold = new GestureManifold(context, mResultListener, mState, handler);
         // Play the role of touch explorer in updating the shared state.
         when(mResultListener.onGestureStarted()).thenReturn(onGestureStarted());
-
-
     }
 
-
     @Test
     public void testRecognizeGesturePath() {
-        final int d = 1000;  // Length of each segment in the test gesture, in pixels.
+        final int d = 1000; // Length of each segment in the test gesture, in pixels.
 
         testPath(p(-d, +0), AccessibilityService.GESTURE_SWIPE_LEFT);
         testPath(p(+d, +0), AccessibilityService.GESTURE_SWIPE_RIGHT);
@@ -138,9 +138,10 @@
 
         // For each path step from start (non-inclusive) to end ... add a motion point.
         for (int step = 1; step < numSteps; ++step) {
-            path.add(new PointF(
-                    (start.x + (stepX * (float) step)),
-                    (start.y + (stepY * (float) step))));
+            path.add(
+                    new PointF(
+                            (start.x + (stepX * (float) step)),
+                            (start.y + (stepY * (float) step))));
         }
     }
 
@@ -164,11 +165,12 @@
             } else if (pointIndex == path.size() - 1) {
                 action = MotionEvent.ACTION_UP;
             }
-            MotionEvent event = MotionEvent.obtain(eventDownTimeMs, eventTimeMs, action,
-                    point.x, point.y, 0);
+            MotionEvent event =
+                    MotionEvent.obtain(eventDownTimeMs, eventTimeMs, action, point.x, point.y, 0);
 
             // Send event.
-            mState.onReceivedMotionEvent(event);
+            // In this case the event and raw event values are the same.
+            mState.onReceivedMotionEvent(event, event, policyFlags);
             mManifold.onMotionEvent(event, event, policyFlags);
             eventTimeMs += PATH_STEP_MILLISEC;
             if (mState.isClear()) {
@@ -178,8 +180,9 @@
 
         mState.clear();
         // Check that correct gesture was recognized.
-        verify(mResultListener).onGestureCompleted(
-                argThat(gestureEvent -> gestureEvent.getGestureId() == gestureId));
+        verify(mResultListener)
+                .onGestureCompleted(
+                        argThat(gestureEvent -> gestureEvent.getGestureId() == gestureId));
     }
 
     private boolean onGestureStarted() {