diff options
author | Ameer Armaly <aarmaly@google.com> | 2020-06-01 18:12:54 -0700 |
---|---|---|
committer | Ameer Armaly <aarmaly@google.com> | 2020-06-22 13:58:51 -0700 |
commit | 9e83d078248c8a6a7aa0cefb4457234a244a986c (patch) | |
tree | 17549f52bbe89de319ed69702b8a82b3a173e02e | |
parent | 3d4bab487086fb389ee4130f3894393c8d27de7a (diff) |
[DO NOT MERGE] Bring back touch events for double tap and double tap and hold.
Bug: 159168795
Test: atest TouchExplorerTest
Change-Id: I427b98c71ce8a2ac5b9285b2f34c1864f48c4a32
9 files changed, 524 insertions, 103 deletions
diff --git a/core/java/android/view/MotionEvent.java b/core/java/android/view/MotionEvent.java index 19eff72ca814..51b0c6b59f3c 100644 --- a/core/java/android/view/MotionEvent.java +++ b/core/java/android/view/MotionEvent.java @@ -487,6 +487,21 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int FLAG_TAINTED = 0x80000000; /** + * Private flag indicating that this event was synthesized by the system and should be delivered + * to the accessibility focused view first. When being dispatched such an event is not handled + * by predecessors of the accessibility focused view and after the event reaches that view the + * flag is cleared and normal event dispatch is performed. This ensures that the platform can + * click on any view that has accessibility focus which is semantically equivalent to asking the + * view to perform a click accessibility action but more generic as views not implementing click + * action correctly can still be activated. + * + * @hide + * @see #isTargetAccessibilityFocus() + * @see #setTargetAccessibilityFocus(boolean) + */ + public static final int FLAG_TARGET_ACCESSIBILITY_FOCUS = 0x40000000; + + /** * Flag indicating the motion event intersected the top edge of the screen. */ public static final int EDGE_TOP = 0x00000001; @@ -2140,6 +2155,20 @@ public final class MotionEvent extends InputEvent implements Parcelable { } /** @hide */ + public boolean isTargetAccessibilityFocus() { + final int flags = getFlags(); + return (flags & FLAG_TARGET_ACCESSIBILITY_FOCUS) != 0; + } + + /** @hide */ + public void setTargetAccessibilityFocus(boolean targetsFocus) { + final int flags = getFlags(); + nativeSetFlags(mNativePtr, targetsFocus + ? flags | FLAG_TARGET_ACCESSIBILITY_FOCUS + : flags & ~FLAG_TARGET_ACCESSIBILITY_FOCUS); + } + + /** @hide */ public final boolean isHoverExitPending() { final int flags = getFlags(); return (flags & FLAG_HOVER_EXIT_PENDING) != 0; diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 1226202dfdf9..df1c672eb9eb 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -14274,6 +14274,14 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ public boolean dispatchTouchEvent(MotionEvent event) { // If the event should be handled by accessibility focus first. + if (event.isTargetAccessibilityFocus()) { + // We don't have focus or no virtual descendant has it, do not handle the event. + if (!isAccessibilityFocusedViewOrHost()) { + return false; + } + // We have focus and got the event, then use normal event dispatch. + event.setTargetAccessibilityFocus(false); + } boolean result = false; if (mInputEventConsistencyVerifier != null) { diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java index e3362aafbcd4..77fedd7c30d4 100644 --- a/core/java/android/view/ViewGroup.java +++ b/core/java/android/view/ViewGroup.java @@ -2048,8 +2048,26 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager for (int i = childrenCount - 1; i >= 0; i--) { final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder); final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex); + View childWithAccessibilityFocus = + event.isTargetAccessibilityFocus() + ? findChildWithAccessibilityFocus() + : null; + if (!child.canReceivePointerEvents() || !isTransformedTouchPointInView(x, y, child, null)) { + + // If there is a view that has accessibility focus we want it + // to get the event first and if not handled we will perform a + // normal dispatch. We may do a double iteration but this is + // safer given the timeframe. + if (childWithAccessibilityFocus != null) { + if (childWithAccessibilityFocus != child) { + continue; + } + childWithAccessibilityFocus = null; + i = childrenCount - 1; + } + event.setTargetAccessibilityFocus(false); continue; } final PointerIcon pointerIcon = @@ -2617,6 +2635,12 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager mInputEventConsistencyVerifier.onTouchEvent(ev, 1); } + // If the event targets the accessibility focused view and this is it, start + // normal event dispatch. Maybe a descendant is what will handle the click. + if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) { + ev.setTargetAccessibilityFocus(false); + } + boolean handled = false; if (onFilterTouchEventForSecurity(ev)) { final int action = ev.getAction(); @@ -2647,6 +2671,13 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager // so this view group continues to intercept touches. intercepted = true; } + + // If intercepted, start normal event dispatch. Also if there is already + // a view that is handling the gesture, do normal event dispatch. + if (intercepted || mFirstTouchTarget != null) { + ev.setTargetAccessibilityFocus(false); + } + // Check for cancelation. final boolean canceled = resetCancelNextUpFlag(this) || actionMasked == MotionEvent.ACTION_CANCEL; @@ -2658,6 +2689,14 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager TouchTarget newTouchTarget = null; boolean alreadyDispatchedToNewTouchTarget = false; if (!canceled && !intercepted) { + // If the event is targeting accessibility focus we give it to the + // view that has accessibility focus and if it does not handle it + // we clear the flag and dispatch the event to all children as usual. + // We are looking up the accessibility focused host to avoid keeping + // state since these events are very rare. + View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus() + ? findChildWithAccessibilityFocus() : null; + if (actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { @@ -2720,6 +2759,10 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager alreadyDispatchedToNewTouchTarget = true; break; } + + // The accessibility focus didn't handle the event, so clear + // the flag and do a normal dispatch to all children. + ev.setTargetAccessibilityFocus(false); } if (preorderedList != null) preorderedList.clear(); } @@ -2803,6 +2846,34 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager return buildOrderedChildList(); } + /** + * Finds the child which has accessibility focus. + * + * @return The child that has focus. + */ + private View findChildWithAccessibilityFocus() { + ViewRootImpl viewRoot = getViewRootImpl(); + if (viewRoot == null) { + return null; + } + + View current = viewRoot.getAccessibilityFocusedHost(); + if (current == null) { + return null; + } + + ViewParent parent = current.getParent(); + while (parent instanceof View) { + if (parent == this) { + return current; + } + current = (View) parent; + parent = current.getParent(); + } + + return null; + } + /** * Resets all touch state in preparation for a new cycle. */ @@ -3257,9 +3328,10 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager break; } default: - throw new IllegalStateException("descendant focusability must be " - + "one of FOCUS_BEFORE_DESCENDANTS, FOCUS_AFTER_DESCENDANTS, FOCUS_BLOCK_DESCENDANTS " - + "but is " + descendantFocusability); + throw new IllegalStateException( + "descendant focusability must be one of FOCUS_BEFORE_DESCENDANTS," + + " FOCUS_AFTER_DESCENDANTS, FOCUS_BLOCK_DESCENDANTS but is " + + descendantFocusability); } if (result && !isLayoutValid() && ((mPrivateFlags & PFLAG_WANTS_FOCUS) == 0)) { mPrivateFlags |= PFLAG_WANTS_FOCUS; @@ -4925,7 +4997,8 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager if (params == null) { params = generateDefaultLayoutParams(); if (params == null) { - throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null"); + throw new IllegalArgumentException( + "generateDefaultLayoutParams() cannot return null"); } } addView(child, index, params); diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index 7f912a4fc1ce..d2b1bd1a6008 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -56,6 +56,8 @@ import android.content.pm.PackageManagerInternal; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; import android.database.ContentObserver; +import android.graphics.Point; +import android.graphics.Rect; import android.graphics.Region; import android.hardware.display.DisplayManager; import android.hardware.fingerprint.IFingerprintService; @@ -190,6 +192,9 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub private final SimpleStringSplitter mStringColonSplitter = new SimpleStringSplitter(COMPONENT_NAME_SEPARATOR); + private final Rect mTempRect = new Rect(); + private final Rect mTempRect1 = new Rect(); + private final PackageManager mPackageManager; private final PowerManager mPowerManager; @@ -246,6 +251,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub //TODO: Remove this hack private boolean mInitialized; + private Point mTempPoint; private boolean mIsAccessibilityButtonShown; private AccessibilityUserState getCurrentUserStateLocked() { @@ -1068,6 +1074,18 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } /** + * Gets a point within the accessibility focused node where we can send down + * and up events to perform a click. + * + * @param outPoint The click point to populate. + * @return Whether accessibility a click point was found and set. + */ + // TODO: (multi-display) Make sure this works for multiple displays. + public boolean getAccessibilityFocusClickPointInScreen(Point outPoint) { + return getInteractionBridge().getAccessibilityFocusClickPointInScreenNotLocked(outPoint); + } + + /** * Perform an accessibility action on the view that currently has accessibility focus. * Has no effect if no item has accessibility focus, if the item with accessibility * focus does not expose the specified action, or if the action fails. @@ -1081,6 +1099,32 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub return getInteractionBridge().performActionOnAccessibilityFocusedItemNotLocked(action); } + /** + * Returns true if accessibility focus is confined to the active window. + */ + public boolean accessibilityFocusOnlyInActiveWindow() { + synchronized (mLock) { + return mA11yWindowManager.isTrackingWindowsLocked(); + } + } + + /** + * Gets the bounds of a window. + * + * @param outBounds The output to which to write the bounds. + */ + boolean getWindowBounds(int windowId, Rect outBounds) { + IBinder token; + synchronized (mLock) { + token = getWindowToken(windowId, mCurrentUserId); + } + mWindowManagerService.getWindowFrame(token, outBounds); + if (!outBounds.isEmpty()) { + return true; + } + return false; + } + public int getActiveWindowId() { return mA11yWindowManager.getActiveWindowId(mCurrentUserId); } @@ -1824,9 +1868,11 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub for (int i = 0; !observingWindows && (i < boundServiceCount); i++) { AccessibilityServiceConnection boundService = boundServices.get(i); if (boundService.canRetrieveInteractiveWindowsLocked()) { + userState.setAccessibilityFocusOnlyInActiveWindow(false); observingWindows = true; } } + userState.setAccessibilityFocusOnlyInActiveWindow(true); // Gets all valid displays and start tracking windows of each display if there is at least // one bound service that can retrieve window content. @@ -2930,6 +2976,19 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } /** + * Gets a point within the accessibility focused node where we can send down and up events + * to perform a click. + * + * @param outPoint The click point to populate. + * @return Whether accessibility a click point was found and set. + */ + // TODO: (multi-display) Make sure this works for multiple displays. + boolean getAccessibilityFocusClickPointInScreen(Point outPoint) { + return getInteractionBridge() + .getAccessibilityFocusClickPointInScreenNotLocked(outPoint); + } + + /** * Perform an accessibility action on the view that currently has accessibility focus. * Has no effect if no item has accessibility focus, if the item with accessibility * focus does not expose the specified action, or if the action fails. @@ -2947,6 +3006,43 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub return focus.performAction(action.getId()); } + public boolean getAccessibilityFocusClickPointInScreenNotLocked(Point outPoint) { + AccessibilityNodeInfo focus = getAccessibilityFocusNotLocked(); + if (focus == null) { + return false; + } + + synchronized (mLock) { + Rect boundsInScreen = mTempRect; + focus.getBoundsInScreen(boundsInScreen); + + // Apply magnification if needed. + MagnificationSpec spec = getCompatibleMagnificationSpecLocked(focus.getWindowId()); + if (spec != null && !spec.isNop()) { + boundsInScreen.offset((int) -spec.offsetX, (int) -spec.offsetY); + boundsInScreen.scale(1 / spec.scale); + } + + // Clip to the window bounds. + Rect windowBounds = mTempRect1; + getWindowBounds(focus.getWindowId(), windowBounds); + if (!boundsInScreen.intersect(windowBounds)) { + return false; + } + + // Clip to the screen bounds. + Point screenSize = mTempPoint; + mDefaultDisplay.getRealSize(screenSize); + if (!boundsInScreen.intersect(0, 0, screenSize.x, screenSize.y)) { + return false; + } + + outPoint.set(boundsInScreen.centerX(), boundsInScreen.centerY()); + } + + return true; + } + private AccessibilityNodeInfo getAccessibilityFocusNotLocked() { final int focusedWindowId; synchronized (mLock) { diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java index 43bb4b384bb2..0845d019c060 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java @@ -104,6 +104,7 @@ class AccessibilityUserState { private boolean mIsDisplayMagnificationEnabled; private boolean mIsFilterKeyEventsEnabled; private boolean mIsPerformGesturesEnabled; + private boolean mAccessibilityFocusOnlyInActiveWindow; private boolean mIsTextHighContrastEnabled; private boolean mIsTouchExplorationEnabled; private boolean mServiceHandlesDoubleTap; @@ -685,6 +686,13 @@ class AccessibilityUserState { mIsPerformGesturesEnabled = enabled; } + public boolean isAccessibilityFocusOnlyInActiveWindow() { + return mAccessibilityFocusOnlyInActiveWindow; + } + + public void setAccessibilityFocusOnlyInActiveWindow(boolean enabled) { + mAccessibilityFocusOnlyInActiveWindow = enabled; + } public ComponentName getServiceChangingSoftKeyboardModeLocked() { return mServiceChangingSoftKeyboardMode; } 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 667364c9c901..c8cee1079e8e 100644 --- a/services/accessibility/java/com/android/server/accessibility/gestures/EventDispatcher.java +++ b/services/accessibility/java/com/android/server/accessibility/gestures/EventDispatcher.java @@ -21,8 +21,11 @@ import static com.android.server.accessibility.gestures.TouchState.ALL_POINTER_I import static com.android.server.accessibility.gestures.TouchState.MAX_POINTER_COUNT; import android.content.Context; +import android.graphics.Point; import android.util.Slog; import android.view.MotionEvent; +import android.view.MotionEvent.PointerCoords; +import android.view.MotionEvent.PointerProperties; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; @@ -37,19 +40,27 @@ import com.android.server.policy.WindowManagerPolicy; */ class EventDispatcher { private static final String LOG_TAG = "EventDispatcher"; + private static final int CLICK_LOCATION_NONE = 0; + private static final int CLICK_LOCATION_ACCESSIBILITY_FOCUS = 1; + private static final int CLICK_LOCATION_LAST_TOUCH_EXPLORED = 2; private final AccessibilityManagerService mAms; private Context mContext; // The receiver of motion events. private EventStreamTransformation mReceiver; - // Keep track of which pointers sent to the system are down. - private int mInjectedPointersDown; - // The time of the last injected down. - private long mLastInjectedDownEventTime; + // The long pressing pointer id if coordinate remapping is needed for double tap and hold + private int mLongPressingPointerId = -1; + + // The long pressing pointer X if coordinate remapping is needed for double tap and hold. + private int mLongPressingPointerDeltaX; + + // The long pressing pointer Y if coordinate remapping is needed for double tap and hold. + private int mLongPressingPointerDeltaY; + + // Temporary point to avoid instantiation. + private final Point mTempPoint = new Point(); - // The last injected hover event. - private MotionEvent mLastInjectedHoverEvent; private TouchState mState; EventDispatcher( @@ -98,8 +109,18 @@ class EventDispatcher { if (action == MotionEvent.ACTION_DOWN) { event.setDownTime(event.getEventTime()); } else { - event.setDownTime(getLastInjectedDownEventTime()); + event.setDownTime(mState.getLastInjectedDownEventTime()); + } + // If the user is long pressing but the long pressing pointer + // was not exactly over the accessibility focused item we need + // to remap the location of that pointer so the user does not + // have to explicitly touch explore something to be able to + // long press it, or even worse to avoid the user long pressing + // on the wrong item since click and long press behave differently. + if (mLongPressingPointerId >= 0) { + event = offsetEvent(event, -mLongPressingPointerDeltaX, -mLongPressingPointerDeltaY); } + if (DEBUG) { Slog.d( LOG_TAG, @@ -116,7 +137,7 @@ class EventDispatcher { } else { Slog.e(LOG_TAG, "Error sending event: no receiver specified."); } - updateState(event); + mState.onInjectedMotionEvent(event); if (event != prototype) { event.recycle(); @@ -145,87 +166,15 @@ class EventDispatcher { mState.onInjectedAccessibilityEvent(type); } - /** - * Processes an injected {@link MotionEvent} event. - * - * @param event The event to process. - */ - void updateState(MotionEvent event) { - final int action = event.getActionMasked(); - final int pointerId = event.getPointerId(event.getActionIndex()); - final int pointerFlag = (1 << pointerId); - switch (action) { - case MotionEvent.ACTION_DOWN: - case MotionEvent.ACTION_POINTER_DOWN: - mInjectedPointersDown |= pointerFlag; - mLastInjectedDownEventTime = event.getDownTime(); - break; - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_POINTER_UP: - mInjectedPointersDown &= ~pointerFlag; - if (mInjectedPointersDown == 0) { - mLastInjectedDownEventTime = 0; - } - break; - case MotionEvent.ACTION_HOVER_ENTER: - case MotionEvent.ACTION_HOVER_MOVE: - case MotionEvent.ACTION_HOVER_EXIT: - if (mLastInjectedHoverEvent != null) { - mLastInjectedHoverEvent.recycle(); - } - mLastInjectedHoverEvent = MotionEvent.obtain(event); - break; - } - if (DEBUG) { - Slog.i(LOG_TAG, "Injected pointer:\n" + toString()); - } - } - - /** Clears the internals state. */ - public void clear() { - mInjectedPointersDown = 0; - } - - /** @return The time of the last injected down event. */ - public long getLastInjectedDownEventTime() { - return mLastInjectedDownEventTime; - } - - /** @return The number of down pointers injected to the view hierarchy. */ - public int getInjectedPointerDownCount() { - return Integer.bitCount(mInjectedPointersDown); - } - - /** @return The bits of the injected pointers that are down. */ - public int getInjectedPointersDown() { - return mInjectedPointersDown; - } - - /** - * Whether an injected pointer is down. - * - * @param pointerId The unique pointer id. - * @return True if the pointer is down. - */ - public boolean isInjectedPointerDown(int pointerId) { - final int pointerFlag = (1 << pointerId); - return (mInjectedPointersDown & pointerFlag) != 0; - } - - /** @return The the last injected hover event. */ - public MotionEvent getLastInjectedHoverEvent() { - return mLastInjectedHoverEvent; - } - @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append("========================="); builder.append("\nDown pointers #"); - builder.append(Integer.bitCount(mInjectedPointersDown)); + builder.append(Integer.bitCount(mState.getInjectedPointersDown())); builder.append(" [ "); for (int i = 0; i < MAX_POINTER_COUNT; i++) { - if ((mInjectedPointersDown & i) != 0) { + if (mState.isInjectedPointerDown(i)) { builder.append(i); builder.append(" "); } @@ -236,6 +185,48 @@ class EventDispatcher { } /** + * /** Offsets all pointers in the given event by adding the specified X and Y offsets. + * + * @param event The event to offset. + * @param offsetX The X offset. + * @param offsetY The Y offset. + * @return An event with the offset pointers or the original event if both offsets are zero. + */ + private MotionEvent offsetEvent(MotionEvent event, int offsetX, int offsetY) { + if (offsetX == 0 && offsetY == 0) { + return event; + } + final int remappedIndex = event.findPointerIndex(mLongPressingPointerId); + final int pointerCount = event.getPointerCount(); + PointerProperties[] props = PointerProperties.createArray(pointerCount); + PointerCoords[] coords = PointerCoords.createArray(pointerCount); + for (int i = 0; i < pointerCount; i++) { + event.getPointerProperties(i, props[i]); + event.getPointerCoords(i, coords[i]); + if (i == remappedIndex) { + coords[i].x += offsetX; + coords[i].y += offsetY; + } + } + return MotionEvent.obtain( + event.getDownTime(), + event.getEventTime(), + event.getAction(), + event.getPointerCount(), + props, + coords, + event.getMetaState(), + event.getButtonState(), + 1.0f, + 1.0f, + event.getDeviceId(), + event.getEdgeFlags(), + event.getSource(), + event.getDisplayId(), + event.getFlags()); + } + + /** * Computes the action for an injected event based on a masked action and a pointer index. * * @param actionMasked The masked action. @@ -247,7 +238,7 @@ class EventDispatcher { case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_POINTER_DOWN: // Compute the action based on how many down pointers are injected. - if (getInjectedPointerDownCount() == 0) { + if (mState.getInjectedPointerDownCount() == 0) { return MotionEvent.ACTION_DOWN; } else { return (pointerIndex << MotionEvent.ACTION_POINTER_INDEX_SHIFT) @@ -255,7 +246,7 @@ class EventDispatcher { } case MotionEvent.ACTION_POINTER_UP: // Compute the action based on how many down pointers are injected. - if (getInjectedPointerDownCount() == 1) { + if (mState.getInjectedPointerDownCount() == 1) { return MotionEvent.ACTION_UP; } else { return (pointerIndex << MotionEvent.ACTION_POINTER_INDEX_SHIFT) @@ -280,7 +271,7 @@ class EventDispatcher { for (int i = 0; i < pointerCount; i++) { final int pointerId = prototype.getPointerId(i); // Do not send event for already delivered pointers. - if (!isInjectedPointerDown(pointerId)) { + if (!mState.isInjectedPointerDown(pointerId)) { pointerIdBits |= (1 << pointerId); final int action = computeInjectionAction(MotionEvent.ACTION_DOWN, i); sendMotionEvent( @@ -306,7 +297,7 @@ class EventDispatcher { for (int i = 0; i < pointerCount; i++) { final int pointerId = prototype.getPointerId(i); // Skip non injected down pointers. - if (!isInjectedPointerDown(pointerId)) { + if (!mState.isInjectedPointerDown(pointerId)) { continue; } final int action = computeInjectionAction(MotionEvent.ACTION_POINTER_UP, i); @@ -315,4 +306,97 @@ class EventDispatcher { pointerIdBits &= ~(1 << pointerId); } } + + 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; + } + + public void clickWithTouchEvents(MotionEvent event, MotionEvent rawEvent, 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) { + Slog.e(LOG_TAG, "Unable to compute click location."); + // We can't send a click to no location, but the gesture was still + // consumed. + return; + } + // Do the click. + PointerProperties[] properties = new PointerProperties[1]; + properties[0] = new PointerProperties(); + event.getPointerProperties(pointerIndex, properties[0]); + PointerCoords[] coords = new PointerCoords[1]; + coords[0] = new PointerCoords(); + coords[0].x = clickLocation.x; + coords[0].y = clickLocation.y; + MotionEvent clickEvent = + MotionEvent.obtain( + event.getDownTime(), + event.getEventTime(), + MotionEvent.ACTION_DOWN, + 1, + properties, + coords, + 0, + 0, + 1.0f, + 1.0f, + event.getDeviceId(), + 0, + event.getSource(), + event.getDisplayId(), + event.getFlags()); + final boolean targetAccessibilityFocus = (result == CLICK_LOCATION_ACCESSIBILITY_FOCUS); + sendActionDownAndUp(clickEvent, rawEvent, policyFlags, targetAccessibilityFocus); + clickEvent.recycle(); + } + + private int computeClickLocation(Point outLocation) { + if (mState.getLastInjectedHoverEventForClick() != null) { + final int lastExplorePointerIndex = + mState.getLastInjectedHoverEventForClick().getActionIndex(); + outLocation.x = + (int) mState.getLastInjectedHoverEventForClick().getX(lastExplorePointerIndex); + outLocation.y = + (int) mState.getLastInjectedHoverEventForClick().getY(lastExplorePointerIndex); + if (!mAms.accessibilityFocusOnlyInActiveWindow() + || mState.getLastTouchedWindowId() == mAms.getActiveWindowId()) { + if (mAms.getAccessibilityFocusClickPointInScreen(outLocation)) { + return CLICK_LOCATION_ACCESSIBILITY_FOCUS; + } else { + return CLICK_LOCATION_LAST_TOUCH_EXPLORED; + } + } + } + if (mAms.getAccessibilityFocusClickPointInScreen(outLocation)) { + return CLICK_LOCATION_ACCESSIBILITY_FOCUS; + } + return CLICK_LOCATION_NONE; + } + + private void sendActionDownAndUp( + MotionEvent prototype, + MotionEvent rawEvent, + int policyFlags, + boolean targetAccessibilityFocus) { + // Tap with the pointer that last explored. + final int pointerId = prototype.getPointerId(prototype.getActionIndex()); + final int pointerIdBits = (1 << pointerId); + prototype.setTargetAccessibilityFocus(targetAccessibilityFocus); + sendMotionEvent(prototype, MotionEvent.ACTION_DOWN, rawEvent, pointerIdBits, policyFlags); + prototype.setTargetAccessibilityFocus(targetAccessibilityFocus); + sendMotionEvent(prototype, MotionEvent.ACTION_UP, rawEvent, pointerIdBits, policyFlags); + } } diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/GestureManifold.java b/services/accessibility/java/com/android/server/accessibility/gestures/GestureManifold.java index 6d0f069e51ac..e9c70c60a322 100644 --- a/services/accessibility/java/com/android/server/accessibility/gestures/GestureManifold.java +++ b/services/accessibility/java/com/android/server/accessibility/gestures/GestureManifold.java @@ -104,6 +104,7 @@ class GestureManifold implements GestureMatcher.StateChangeListener { mHandler = new Handler(context.getMainLooper()); mListener = listener; mState = state; + mMultiFingerGesturesEnabled = false; // Set up gestures. // Start with double tap. mGestures.add(new MultiTap(context, 2, GESTURE_DOUBLE_TAP, this)); @@ -247,7 +248,7 @@ class GestureManifold implements GestureMatcher.StateChangeListener { * and hold is dispatched via onGestureCompleted. Otherwise, this method is called when the * user has performed a double tap and then held down the second tap. */ - void onDoubleTapAndHold(); + void onDoubleTapAndHold(MotionEvent event, MotionEvent rawEvent, int policyFlags); /** * When FLAG_SERVICE_HANDLES_DOUBLE_TAP is enabled, this method is not called; double-tap is @@ -256,7 +257,7 @@ class GestureManifold implements GestureMatcher.StateChangeListener { * * @return true if the event is consumed, else false */ - boolean onDoubleTap(); + boolean onDoubleTap(MotionEvent event, MotionEvent rawEvent, int policyFlags); /** * Called when the system has decided the event stream is a potential gesture. @@ -322,7 +323,7 @@ class GestureManifold implements GestureMatcher.StateChangeListener { new AccessibilityGestureEvent(gestureId, event.getDisplayId()); mListener.onGestureCompleted(gestureEvent); } else { - mListener.onDoubleTap(); + mListener.onDoubleTap(event, rawEvent, policyFlags); } clear(); break; @@ -332,7 +333,7 @@ class GestureManifold implements GestureMatcher.StateChangeListener { new AccessibilityGestureEvent(gestureId, event.getDisplayId()); mListener.onGestureCompleted(gestureEvent); } else { - mListener.onDoubleTapAndHold(); + mListener.onDoubleTapAndHold(event, rawEvent, policyFlags); } clear(); break; 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 4fee672a8803..373d47ed366b 100644 --- a/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java +++ b/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java @@ -22,7 +22,6 @@ import static com.android.server.accessibility.gestures.TouchState.ALL_POINTER_I import android.accessibilityservice.AccessibilityGestureEvent; import android.content.Context; -import android.graphics.Point; import android.graphics.Region; import android.os.Handler; import android.util.Slog; @@ -86,6 +85,7 @@ public class TouchExplorer extends BaseEventStreamTransformation // The ID of the pointer used for dragging. private int mDraggingPointerId; + // Handler for performing asynchronous operations. private final Handler mHandler; @@ -115,8 +115,6 @@ public class TouchExplorer extends BaseEventStreamTransformation // Handle to the accessibility manager service. private final AccessibilityManagerService mAms; - // Temporary point to avoid instantiation. - private final Point mTempPoint = new Point(); // Context in which this explorer operates. private final Context mContext; @@ -277,6 +275,7 @@ public class TouchExplorer extends BaseEventStreamTransformation if (eventType == AccessibilityEvent.TYPE_VIEW_HOVER_EXIT) { sendsPendingA11yEventsIfNeed(); } + mState.onReceivedAccessibilityEvent(event); super.onAccessibilityEvent(event); } @@ -309,16 +308,20 @@ public class TouchExplorer extends BaseEventStreamTransformation } @Override - public void onDoubleTapAndHold() { + public void onDoubleTapAndHold(MotionEvent event, MotionEvent rawEvent, int policyFlags) { // Try to use the standard accessibility API to long click if (!mAms.performActionOnAccessibilityFocusedItem( AccessibilityNodeInfo.AccessibilityAction.ACTION_LONG_CLICK)) { Slog.e(LOG_TAG, "ACTION_LONG_CLICK failed."); + if (mDispatcher.longPressWithTouchEvents(event, policyFlags)) { + sendHoverExitAndTouchExplorationGestureEndIfNeeded(policyFlags); + mState.startDelegating(); + } } } @Override - public boolean onDoubleTap() { + public boolean onDoubleTap(MotionEvent event, MotionEvent rawEvent, int policyFlags) { mAms.onTouchInteractionEnd(); // Remove pending event deliveries. mSendHoverEnterAndMoveDelayed.cancel(); @@ -334,7 +337,10 @@ public class TouchExplorer extends BaseEventStreamTransformation // Try to use the standard accessibility API to click if (!mAms.performActionOnAccessibilityFocusedItem( AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK)) { - Slog.e(LOG_TAG, "ACTION_CLICK failed."); + Slog.e(LOG_TAG, "ACTION_CLICK failed. Dispatching motion events to simulate click."); + + mDispatcher.clickWithTouchEvents(event, rawEvent, policyFlags); + return true; } return true; } @@ -840,7 +846,7 @@ public class TouchExplorer extends BaseEventStreamTransformation * @param policyFlags The policy flags associated with the event. */ private void sendHoverExitAndTouchExplorationGestureEndIfNeeded(int policyFlags) { - MotionEvent event = mDispatcher.getLastInjectedHoverEvent(); + MotionEvent event = mState.getLastInjectedHoverEvent(); if (event != null && event.getActionMasked() != MotionEvent.ACTION_HOVER_EXIT) { final int pointerIdBits = event.getPointerIdBits(); if (!mSendTouchExplorationEndDelayed.isPending()) { @@ -862,7 +868,7 @@ public class TouchExplorer extends BaseEventStreamTransformation * @param policyFlags The policy flags associated with the event. */ private void sendTouchExplorationGestureStartAndHoverEnterIfNeeded(int policyFlags) { - MotionEvent event = mDispatcher.getLastInjectedHoverEvent(); + MotionEvent event = mState.getLastInjectedHoverEvent(); if (event != null && event.getActionMasked() == MotionEvent.ACTION_HOVER_EXIT) { final int pointerIdBits = event.getPointerIdBits(); mDispatcher.sendMotionEvent( @@ -1188,7 +1194,6 @@ public class TouchExplorer extends BaseEventStreamTransformation + ", mDetermineUserIntentTimeout: " + mDetermineUserIntentTimeout + ", mDoubleTapSlop: " + mDoubleTapSlop + ", mDraggingPointerId: " + mDraggingPointerId - + ", mTempPoint: " + mTempPoint + " }"; } } 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 d23dbbefd325..7a39bc29e8e5 100644 --- a/services/accessibility/java/com/android/server/accessibility/gestures/TouchState.java +++ b/services/accessibility/java/com/android/server/accessibility/gestures/TouchState.java @@ -75,6 +75,16 @@ public class TouchState { private MotionEvent mLastReceivedEvent; // The accompanying raw event without any transformations. private MotionEvent mLastReceivedRawEvent; + // The id of the last touch explored window. + private int mLastTouchedWindowId; + // The last injected hover event. + private MotionEvent mLastInjectedHoverEvent; + // The last injected hover event used for performing clicks. + private MotionEvent mLastInjectedHoverEventForClick; + // The time of the last injected down. + private long mLastInjectedDownEventTime; + // Keep track of which pointers sent to the system are down. + private int mInjectedPointersDown; public TouchState() { mReceivedPointerTracker = new ReceivedPointerTracker(); @@ -88,7 +98,9 @@ public class TouchState { mLastReceivedEvent.recycle(); mLastReceivedEvent = null; } + mLastTouchedWindowId = -1; mReceivedPointerTracker.clear(); + mInjectedPointersDown = 0; } /** @@ -107,6 +119,71 @@ public class TouchState { mReceivedPointerTracker.onMotionEvent(rawEvent); } + /** + * Processes an injected {@link MotionEvent} event. + * + * @param event The event to process. + */ + void onInjectedMotionEvent(MotionEvent event) { + final int action = event.getActionMasked(); + final int pointerId = event.getPointerId(event.getActionIndex()); + final int pointerFlag = (1 << pointerId); + switch (action) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_POINTER_DOWN: + mInjectedPointersDown |= pointerFlag; + mLastInjectedDownEventTime = event.getDownTime(); + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_POINTER_UP: + mInjectedPointersDown &= ~pointerFlag; + if (mInjectedPointersDown == 0) { + mLastInjectedDownEventTime = 0; + } + break; + case MotionEvent.ACTION_HOVER_ENTER: + case MotionEvent.ACTION_HOVER_MOVE: + if (mLastInjectedHoverEvent != null) { + mLastInjectedHoverEvent.recycle(); + } + mLastInjectedHoverEvent = MotionEvent.obtain(event); + break; + case MotionEvent.ACTION_HOVER_EXIT: + if (mLastInjectedHoverEvent != null) { + mLastInjectedHoverEvent.recycle(); + } + mLastInjectedHoverEvent = MotionEvent.obtain(event); + if (mLastInjectedHoverEventForClick != null) { + mLastInjectedHoverEventForClick.recycle(); + } + mLastInjectedHoverEventForClick = MotionEvent.obtain(event); + break; + } + if (DEBUG) { + Slog.i(LOG_TAG, "Injected pointer:\n" + toString()); + } + } + + /** Updates state in response to an accessibility event received from the outside. */ + public void onReceivedAccessibilityEvent(AccessibilityEvent event) { + // If a new window opens or the accessibility focus moves we no longer + // want to click/long press on the last touch explored location. + switch (event.getEventType()) { + case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED: + case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED: + if (mLastInjectedHoverEventForClick != null) { + mLastInjectedHoverEventForClick.recycle(); + mLastInjectedHoverEventForClick = null; + } + mLastTouchedWindowId = -1; + break; + case AccessibilityEvent.TYPE_VIEW_HOVER_ENTER: + case AccessibilityEvent.TYPE_VIEW_HOVER_EXIT: + mLastTouchedWindowId = event.getWindowId(); + break; + } + } + public void onInjectedAccessibilityEvent(int type) { // The below state transitions go here because the related events are often sent on a // delay. @@ -236,6 +313,46 @@ public class TouchState { return mLastReceivedEvent; } + /** @return The the last injected hover event. */ + public MotionEvent getLastInjectedHoverEvent() { + return mLastInjectedHoverEvent; + } + + /** @return The time of the last injected down event. */ + public long getLastInjectedDownEventTime() { + return mLastInjectedDownEventTime; + } + + public int getLastTouchedWindowId() { + return mLastTouchedWindowId; + } + + /** @return The number of down pointers injected to the view hierarchy. */ + public int getInjectedPointerDownCount() { + return Integer.bitCount(mInjectedPointersDown); + } + + /** @return The bits of the injected pointers that are down. */ + public int getInjectedPointersDown() { + return mInjectedPointersDown; + } + + /** + * Whether an injected pointer is down. + * + * @param pointerId The unique pointer id. + * @return True if the pointer is down. + */ + public boolean isInjectedPointerDown(int pointerId) { + final int pointerFlag = (1 << pointerId); + return (mInjectedPointersDown & pointerFlag) != 0; + } + + /** @return The the last injected hover event used for a click. */ + public MotionEvent getLastInjectedHoverEventForClick() { + return mLastInjectedHoverEventForClick; + } + /** 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"; |