summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAmeer Armaly <aarmaly@google.com>2020-06-01 18:12:54 -0700
committerAmeer Armaly <aarmaly@google.com>2020-06-22 13:58:51 -0700
commit9e83d078248c8a6a7aa0cefb4457234a244a986c (patch)
tree17549f52bbe89de319ed69702b8a82b3a173e02e
parent3d4bab487086fb389ee4130f3894393c8d27de7a (diff)
[DO NOT MERGE] Bring back touch events for double tap and double tap and hold.
Bug: 159168795 Test: atest TouchExplorerTest Change-Id: I427b98c71ce8a2ac5b9285b2f34c1864f48c4a32
-rw-r--r--core/java/android/view/MotionEvent.java29
-rw-r--r--core/java/android/view/View.java8
-rw-r--r--core/java/android/view/ViewGroup.java81
-rw-r--r--services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java96
-rw-r--r--services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java8
-rw-r--r--services/accessibility/java/com/android/server/accessibility/gestures/EventDispatcher.java256
-rw-r--r--services/accessibility/java/com/android/server/accessibility/gestures/GestureManifold.java9
-rw-r--r--services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java23
-rw-r--r--services/accessibility/java/com/android/server/accessibility/gestures/TouchState.java117
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";