summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSvet Ganov <svetoslavganov@google.com>2014-09-03 21:33:00 -0700
committerSvet Ganov <svetoslavganov@google.com>2014-09-07 23:36:20 -0700
commit7498efdc5e163d6b4a11db941c7d13c169d37284 (patch)
tree2d94b4e0fe85ea98a438ecca6184d878ee5e61bd
parentdf11867be3f60b6bd5e24afca7820c29a28c85c7 (diff)
downloadframeworks_base-7498efdc5e163d6b4a11db941c7d13c169d37284.zip
frameworks_base-7498efdc5e163d6b4a11db941c7d13c169d37284.tar.gz
frameworks_base-7498efdc5e163d6b4a11db941c7d13c169d37284.tar.bz2
Clicking on partially coverd views by other views or windows.
In touch exploration mode an accessibility service can move accessibility focus in response to user gestures. In this case when the user double-taps the system is sending down and up events at the center of the acessibility focused view. This works fine until the clicked view's center is covered by another clickable view. In such a scenario the user thinks he is clicking on one view but the click is handled by another. Terrible. This change solves the problem of clicking on the wrong view and also solves the problem of clicking on the wrong window. The key idea is that when the system detects a double tap or a double tap and hold it asks the accessibility focused node (if such) to compute a point at which a click can be performed. In respinse to that the node is asking the source view to compute this. If a view is partially covered by siblings or siblings of predecessors that are clickable, the click point will be properly computed to ensure the click occurs on the desired view. The click point is also bounded in the interactive part of the host window. The current approach has rare edge cases that may produce false positives or false negatives. For example, a portion of the view may be covered by an interactive descendant of a predecessor, which we do not compute (we check only siblings of predecessors). Also a view may be handling raw touch events instead of registering click listeners, which we cannot compute. Despite these limitations this approach will work most of the time and it is a huge improvement over just blindly sending the down and up events in the center of the view. Note that the additional computational complexity is incurred only when the user wants to click on the accessibility focused view which is very a rare event and this is a good tradeoff. bug:15696993 Change-Id: I85927a77d6c24f7550b0d5f9f762722a8230830f
-rw-r--r--core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl4
-rw-r--r--core/java/android/view/AccessibilityInteractionController.java146
-rw-r--r--core/java/android/view/View.java142
-rw-r--r--core/java/android/view/ViewGroup.java106
-rw-r--r--core/java/android/view/ViewRootImpl.java24
-rw-r--r--core/java/android/view/accessibility/AccessibilityInteractionClient.java69
-rw-r--r--core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl5
-rw-r--r--core/java/android/view/accessibility/IAccessibilityInteractionConnectionCallback.aidl9
-rw-r--r--services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java113
-rw-r--r--services/accessibility/java/com/android/server/accessibility/TouchExplorer.java48
10 files changed, 602 insertions, 64 deletions
diff --git a/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl b/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl
index 5f7a17d..27a03b6 100644
--- a/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl
+++ b/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl
@@ -54,6 +54,10 @@ interface IAccessibilityServiceConnection {
int action, in Bundle arguments, int interactionId,
IAccessibilityInteractionConnectionCallback callback, long threadId);
+ boolean computeClickPointInScreen(int accessibilityWindowId, long accessibilityNodeId,
+ int interactionId, IAccessibilityInteractionConnectionCallback callback,
+ long threadId);
+
AccessibilityWindowInfo getWindow(int windowId);
List<AccessibilityWindowInfo> getWindows();
diff --git a/core/java/android/view/AccessibilityInteractionController.java b/core/java/android/view/AccessibilityInteractionController.java
index a10dda3..a283b91 100644
--- a/core/java/android/view/AccessibilityInteractionController.java
+++ b/core/java/android/view/AccessibilityInteractionController.java
@@ -19,7 +19,6 @@ package android.view;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.Region;
-import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
@@ -101,7 +100,7 @@ final class AccessibilityInteractionController {
IAccessibilityInteractionConnectionCallback callback, int flags, int interrogatingPid,
long interrogatingTid, MagnificationSpec spec) {
Message message = mHandler.obtainMessage();
- message.what = PrivateHandler.MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_ACCESSIBILITY_ID;
+ message.what = PrivateHandler.MSG_FIND_ACCESSIBILITY_NODE_INFO_BY_ACCESSIBILITY_ID;
message.arg1 = flags;
SomeArgs args = SomeArgs.obtain();
@@ -176,7 +175,7 @@ final class AccessibilityInteractionController {
IAccessibilityInteractionConnectionCallback callback, int flags, int interrogatingPid,
long interrogatingTid, MagnificationSpec spec) {
Message message = mHandler.obtainMessage();
- message.what = PrivateHandler.MSG_FIND_ACCESSIBLITY_NODE_INFOS_BY_VIEW_ID;
+ message.what = PrivateHandler.MSG_FIND_ACCESSIBILITY_NODE_INFOS_BY_VIEW_ID;
message.arg1 = flags;
message.arg2 = AccessibilityNodeInfo.getAccessibilityViewId(accessibilityNodeId);
@@ -261,7 +260,7 @@ final class AccessibilityInteractionController {
IAccessibilityInteractionConnectionCallback callback, int flags, int interrogatingPid,
long interrogatingTid, MagnificationSpec spec) {
Message message = mHandler.obtainMessage();
- message.what = PrivateHandler.MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_TEXT;
+ message.what = PrivateHandler.MSG_FIND_ACCESSIBILITY_NODE_INFO_BY_TEXT;
message.arg1 = flags;
SomeArgs args = SomeArgs.obtain();
@@ -637,6 +636,95 @@ final class AccessibilityInteractionController {
}
}
+ public void computeClickPointInScreenClientThread(long accessibilityNodeId,
+ Region interactiveRegion, int interactionId,
+ IAccessibilityInteractionConnectionCallback callback, int interrogatingPid,
+ long interrogatingTid, MagnificationSpec spec) {
+ Message message = mHandler.obtainMessage();
+ message.what = PrivateHandler.MSG_COMPUTE_CLICK_POINT_IN_SCREEN;
+
+ SomeArgs args = SomeArgs.obtain();
+ args.argi1 = AccessibilityNodeInfo.getAccessibilityViewId(accessibilityNodeId);
+ args.argi2 = AccessibilityNodeInfo.getVirtualDescendantId(accessibilityNodeId);
+ args.argi3 = interactionId;
+ args.arg1 = callback;
+ args.arg2 = spec;
+ args.arg3 = interactiveRegion;
+
+ message.obj = args;
+
+ // If the interrogation is performed by the same thread as the main UI
+ // thread in this process, set the message as a static reference so
+ // after this call completes the same thread but in the interrogating
+ // client can handle the message to generate the result.
+ if (interrogatingPid == mMyProcessId && interrogatingTid == mMyLooperThreadId) {
+ AccessibilityInteractionClient.getInstanceForThread(
+ interrogatingTid).setSameThreadMessage(message);
+ } else {
+ mHandler.sendMessage(message);
+ }
+ }
+
+ private void computeClickPointInScreenUiThread(Message message) {
+ SomeArgs args = (SomeArgs) message.obj;
+ final int accessibilityViewId = args.argi1;
+ final int virtualDescendantId = args.argi2;
+ final int interactionId = args.argi3;
+ final IAccessibilityInteractionConnectionCallback callback =
+ (IAccessibilityInteractionConnectionCallback) args.arg1;
+ final MagnificationSpec spec = (MagnificationSpec) args.arg2;
+ final Region interactiveRegion = (Region) args.arg3;
+ args.recycle();
+
+ boolean succeeded = false;
+ Point point = mTempPoint;
+ try {
+ if (mViewRootImpl.mView == null || mViewRootImpl.mAttachInfo == null) {
+ return;
+ }
+ View target = null;
+ if (accessibilityViewId != AccessibilityNodeInfo.UNDEFINED_ITEM_ID) {
+ target = findViewByAccessibilityId(accessibilityViewId);
+ } else {
+ target = mViewRootImpl.mView;
+ }
+ if (target != null && isShown(target)) {
+ AccessibilityNodeProvider provider = target.getAccessibilityNodeProvider();
+ if (provider != null) {
+ // For virtual views just use the center of the bounds in screen.
+ AccessibilityNodeInfo node = null;
+ if (virtualDescendantId != AccessibilityNodeInfo.UNDEFINED_ITEM_ID) {
+ node = provider.createAccessibilityNodeInfo(virtualDescendantId);
+ } else {
+ node = provider.createAccessibilityNodeInfo(
+ AccessibilityNodeProvider.HOST_VIEW_ID);
+ }
+ if (node != null) {
+ succeeded = true;
+ Rect boundsInScreen = mTempRect;
+ node.getBoundsInScreen(boundsInScreen);
+ point.set(boundsInScreen.centerX(), boundsInScreen.centerY());
+ }
+ } else if (virtualDescendantId == AccessibilityNodeInfo.UNDEFINED_ITEM_ID) {
+ // For a real view, ask the view to compute the click point.
+ succeeded = target.computeClickPointInScreenForAccessibility(
+ interactiveRegion, point);
+ }
+ }
+ } finally {
+ try {
+ Point result = null;
+ if (succeeded) {
+ applyAppScaleAndMagnificationSpecIfNeeded(point, spec);
+ result = point;
+ }
+ callback.setComputeClickPointInScreenActionResult(result, interactionId);
+ } catch (RemoteException re) {
+ /* ignore - the other side will time out */
+ }
+ }
+ }
+
private View findViewByAccessibilityId(int accessibilityId) {
View root = mViewRootImpl.mView;
if (root == null) {
@@ -688,6 +776,26 @@ final class AccessibilityInteractionController {
}
}
+ private void applyAppScaleAndMagnificationSpecIfNeeded(Point point,
+ MagnificationSpec spec) {
+ final float applicationScale = mViewRootImpl.mAttachInfo.mApplicationScale;
+ if (!shouldApplyAppScaleAndMagnificationSpec(applicationScale, spec)) {
+ return;
+ }
+
+ if (applicationScale != 1.0f) {
+ point.x *= applicationScale;
+ point.y *= applicationScale;
+ }
+
+ if (spec != null) {
+ point.x *= spec.scale;
+ point.y *= spec.scale;
+ point.x += (int) spec.offsetX;
+ point.y += (int) spec.offsetY;
+ }
+ }
+
private void applyAppScaleAndMagnificationSpecIfNeeded(AccessibilityNodeInfo info,
MagnificationSpec spec) {
if (info == null) {
@@ -1080,11 +1188,12 @@ final class AccessibilityInteractionController {
private class PrivateHandler extends Handler {
private final static int MSG_PERFORM_ACCESSIBILITY_ACTION = 1;
- private final static int MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_ACCESSIBILITY_ID = 2;
- private final static int MSG_FIND_ACCESSIBLITY_NODE_INFOS_BY_VIEW_ID = 3;
- private final static int MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_TEXT = 4;
+ private final static int MSG_FIND_ACCESSIBILITY_NODE_INFO_BY_ACCESSIBILITY_ID = 2;
+ private final static int MSG_FIND_ACCESSIBILITY_NODE_INFOS_BY_VIEW_ID = 3;
+ private final static int MSG_FIND_ACCESSIBILITY_NODE_INFO_BY_TEXT = 4;
private final static int MSG_FIND_FOCUS = 5;
private final static int MSG_FOCUS_SEARCH = 6;
+ private final static int MSG_COMPUTE_CLICK_POINT_IN_SCREEN = 7;
public PrivateHandler(Looper looper) {
super(looper);
@@ -1096,16 +1205,18 @@ final class AccessibilityInteractionController {
switch (type) {
case MSG_PERFORM_ACCESSIBILITY_ACTION:
return "MSG_PERFORM_ACCESSIBILITY_ACTION";
- case MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_ACCESSIBILITY_ID:
- return "MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_ACCESSIBILITY_ID";
- case MSG_FIND_ACCESSIBLITY_NODE_INFOS_BY_VIEW_ID:
- return "MSG_FIND_ACCESSIBLITY_NODE_INFOS_BY_VIEW_ID";
- case MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_TEXT:
- return "MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_TEXT";
+ case MSG_FIND_ACCESSIBILITY_NODE_INFO_BY_ACCESSIBILITY_ID:
+ return "MSG_FIND_ACCESSIBILITY_NODE_INFO_BY_ACCESSIBILITY_ID";
+ case MSG_FIND_ACCESSIBILITY_NODE_INFOS_BY_VIEW_ID:
+ return "MSG_FIND_ACCESSIBILITY_NODE_INFOS_BY_VIEW_ID";
+ case MSG_FIND_ACCESSIBILITY_NODE_INFO_BY_TEXT:
+ return "MSG_FIND_ACCESSIBILITY_NODE_INFO_BY_TEXT";
case MSG_FIND_FOCUS:
return "MSG_FIND_FOCUS";
case MSG_FOCUS_SEARCH:
return "MSG_FOCUS_SEARCH";
+ case MSG_COMPUTE_CLICK_POINT_IN_SCREEN:
+ return "MSG_COMPUTE_CLICK_POINT_IN_SCREEN";
default:
throw new IllegalArgumentException("Unknown message type: " + type);
}
@@ -1115,16 +1226,16 @@ final class AccessibilityInteractionController {
public void handleMessage(Message message) {
final int type = message.what;
switch (type) {
- case MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_ACCESSIBILITY_ID: {
+ case MSG_FIND_ACCESSIBILITY_NODE_INFO_BY_ACCESSIBILITY_ID: {
findAccessibilityNodeInfoByAccessibilityIdUiThread(message);
} break;
case MSG_PERFORM_ACCESSIBILITY_ACTION: {
perfromAccessibilityActionUiThread(message);
} break;
- case MSG_FIND_ACCESSIBLITY_NODE_INFOS_BY_VIEW_ID: {
+ case MSG_FIND_ACCESSIBILITY_NODE_INFOS_BY_VIEW_ID: {
findAccessibilityNodeInfosByViewIdUiThread(message);
} break;
- case MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_TEXT: {
+ case MSG_FIND_ACCESSIBILITY_NODE_INFO_BY_TEXT: {
findAccessibilityNodeInfosByTextUiThread(message);
} break;
case MSG_FIND_FOCUS: {
@@ -1133,6 +1244,9 @@ final class AccessibilityInteractionController {
case MSG_FOCUS_SEARCH: {
focusSearchUiThread(message);
} break;
+ case MSG_COMPUTE_CLICK_POINT_IN_SCREEN: {
+ computeClickPointInScreenUiThread(message);
+ } break;
default:
throw new IllegalArgumentException("Unknown message type: " + type);
}
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 92ad4e8..1e12ddc 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -35,6 +35,8 @@ import android.graphics.LinearGradient;
import android.graphics.Matrix;
import android.graphics.Outline;
import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.PathMeasure;
import android.graphics.PixelFormat;
import android.graphics.Point;
import android.graphics.PorterDuff;
@@ -5731,6 +5733,136 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
}
/**
+ * Computes a point on which a sequence of a down/up event can be sent to
+ * trigger clicking this view. This method is for the exclusive use by the
+ * accessibility layer to determine where to send a click event in explore
+ * by touch mode.
+ *
+ * @param interactiveRegion The interactive portion of this window.
+ * @param outPoint The point to populate.
+ * @return True of such a point exists.
+ */
+ boolean computeClickPointInScreenForAccessibility(Region interactiveRegion,
+ Point outPoint) {
+ // Since the interactive portion of the view is a region but as a view
+ // may have a transformation matrix which cannot be applied to a
+ // region we compute the view bounds rectangle and all interactive
+ // predecessor's and sibling's (siblings of predecessors included)
+ // rectangles that intersect the view bounds. At the
+ // end if the view was partially covered by another interactive
+ // view we compute the view's interactive region and pick a point
+ // on its boundary path as regions do not offer APIs to get inner
+ // points. Note that the the code is optimized to fail early and
+ // avoid unnecessary allocations plus computations.
+
+ // The current approach has edge cases that may produce false
+ // positives or false negatives. For example, a portion of the
+ // view may be covered by an interactive descendant of a
+ // predecessor, which we do not compute. Also a view may be handling
+ // raw touch events instead registering click listeners, which
+ // we cannot compute. Despite these limitations this approach will
+ // work most of the time and it is a huge improvement over just
+ // blindly sending the down and up events in the center of the
+ // view.
+
+ // Cannot click on an unattached view.
+ if (mAttachInfo == null) {
+ return false;
+ }
+
+ // Attached to an invisible window means this view is not visible.
+ if (mAttachInfo.mWindowVisibility != View.VISIBLE) {
+ return false;
+ }
+
+ RectF bounds = mAttachInfo.mTmpTransformRect;
+ bounds.set(0, 0, getWidth(), getHeight());
+ List<RectF> intersections = mAttachInfo.mTmpRectList;
+ intersections.clear();
+
+ if (mParent instanceof ViewGroup) {
+ ViewGroup parentGroup = (ViewGroup) mParent;
+ if (!parentGroup.translateBoundsAndIntersectionsInWindowCoordinates(
+ this, bounds, intersections)) {
+ intersections.clear();
+ return false;
+ }
+ }
+
+ // Take into account the window location.
+ final int dx = mAttachInfo.mWindowLeft;
+ final int dy = mAttachInfo.mWindowTop;
+ bounds.offset(dx, dy);
+ offsetRects(intersections, dx, dy);
+
+ if (intersections.isEmpty() && interactiveRegion == null) {
+ outPoint.set((int) bounds.centerX(), (int) bounds.centerY());
+ } else {
+ // This view is partially covered by other views, then compute
+ // the not covered region and pick a point on its boundary.
+ Region region = new Region();
+ region.set((int) bounds.left, (int) bounds.top,
+ (int) bounds.right, (int) bounds.bottom);
+
+ final int intersectionCount = intersections.size();
+ for (int i = intersectionCount - 1; i >= 0; i--) {
+ RectF intersection = intersections.remove(i);
+ region.op((int) intersection.left, (int) intersection.top,
+ (int) intersection.right, (int) intersection.bottom,
+ Region.Op.DIFFERENCE);
+ }
+
+ // If the view is completely covered, done.
+ if (region.isEmpty()) {
+ return false;
+ }
+
+ // Take into account the interactive portion of the window
+ // as the rest is covered by other windows. If no such a region
+ // then the whole window is interactive.
+ if (interactiveRegion != null) {
+ region.op(interactiveRegion, Region.Op.INTERSECT);
+ }
+
+ // If the view is completely covered, done.
+ if (region.isEmpty()) {
+ return false;
+ }
+
+ // Try a shortcut here.
+ if (region.isRect()) {
+ Rect regionBounds = mAttachInfo.mTmpInvalRect;
+ region.getBounds(regionBounds);
+ outPoint.set(regionBounds.centerX(), regionBounds.centerY());
+ return true;
+ }
+
+ // Get the a point on the region boundary path.
+ Path path = region.getBoundaryPath();
+ PathMeasure pathMeasure = new PathMeasure(path, false);
+ final float[] coordinates = mAttachInfo.mTmpTransformLocation;
+
+ // Without loss of generality pick a point.
+ final float point = pathMeasure.getLength() * 0.01f;
+ if (!pathMeasure.getPosTan(point, coordinates, null)) {
+ return false;
+ }
+
+ outPoint.set(Math.round(coordinates[0]), Math.round(coordinates[1]));
+ }
+
+ return true;
+ }
+
+ static void offsetRects(List<RectF> rects, float offsetX, float offsetY) {
+ final int rectCount = rects.size();
+ for (int i = 0; i < rectCount; i++) {
+ RectF intersection = rects.get(i);
+ intersection.offset(offsetX, offsetY);
+ }
+ }
+
+ /**
* Returns the delegate for implementing accessibility support via
* composition. For more details see {@link AccessibilityDelegate}.
*
@@ -20142,6 +20274,16 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
final RectF mTmpTransformRect = new RectF();
/**
+ * Temporary for use in computing hit areas with transformed views
+ */
+ final RectF mTmpTransformRect1 = new RectF();
+
+ /**
+ * Temporary list of rectanges.
+ */
+ final List<RectF> mTmpRectList = new ArrayList<>();
+
+ /**
* Temporary for use in transforming invalidation rect
*/
final Matrix mTmpMatrix = new Matrix();
diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java
index 974fe4e..b4f1910 100644
--- a/core/java/android/view/ViewGroup.java
+++ b/core/java/android/view/ViewGroup.java
@@ -771,6 +771,112 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
}
/**
+ * Translates the given bounds and intersections from child coordinates to
+ * local coordinates. In case any interactive sibling of the calling child
+ * covers the latter, a new intersections is added to the intersection list.
+ * This method is for the exclusive use by the accessibility layer to compute
+ * a point where a sequence of down and up events would click on a view.
+ *
+ * @param child The child making the call.
+ * @param bounds The bounds to translate in child coordinates.
+ * @param intersections The intersections of interactive views covering the child.
+ * @return True if the bounds and intersections were computed, false otherwise.
+ */
+ boolean translateBoundsAndIntersectionsInWindowCoordinates(View child,
+ RectF bounds, List<RectF> intersections) {
+ // Not attached, done.
+ if (mAttachInfo == null) {
+ return false;
+ }
+
+ if (getAlpha() <= 0 || getTransitionAlpha() <= 0 ||
+ getVisibility() != VISIBLE) {
+ // Cannot click on a view with an invisible predecessor.
+ return false;
+ }
+
+ // Compensate for the child transformation.
+ if (!child.hasIdentityMatrix()) {
+ Matrix matrix = child.getMatrix();
+ matrix.mapRect(bounds);
+ final int intersectionCount = intersections.size();
+ for (int i = 0; i < intersectionCount; i++) {
+ RectF intersection = intersections.get(i);
+ matrix.mapRect(intersection);
+ }
+ }
+
+ // Translate the bounds from child to parent coordinates.
+ final int dx = child.mLeft - mScrollX;
+ final int dy = child.mTop - mScrollY;
+ bounds.offset(dx, dy);
+ offsetRects(intersections, dx, dy);
+
+ // If the bounds do not intersect our bounds, done.
+ if (!bounds.intersects(0, 0, getWidth(), getHeight())) {
+ return false;
+ }
+
+ // Check whether any clickable siblings cover the child
+ // view and if so keep track of the intersections. Also
+ // respect Z ordering when iterating over children.
+ ArrayList<View> orderedList = buildOrderedChildList();
+ final boolean useCustomOrder = orderedList == null
+ && isChildrenDrawingOrderEnabled();
+
+ final int childCount = mChildrenCount;
+ for (int i = childCount - 1; i >= 0; i--) {
+ final int childIndex = useCustomOrder
+ ? getChildDrawingOrder(childCount, i) : i;
+ final View sibling = (orderedList == null)
+ ? mChildren[childIndex] : orderedList.get(childIndex);
+
+ // We care only about siblings over the child.
+ if (sibling == child) {
+ break;
+ }
+
+ // If sibling is not interactive we do not care.
+ if (!sibling.isClickable() && !sibling.isLongClickable()) {
+ continue;
+ }
+
+ // Compute the sibling bounds in its coordinates.
+ RectF siblingBounds = mAttachInfo.mTmpTransformRect1;
+ siblingBounds.set(0, 0, sibling.getWidth(), sibling.getHeight());
+
+ // Take into account the sibling transformation matrix.
+ if (!sibling.hasIdentityMatrix()) {
+ sibling.getMatrix().mapRect(siblingBounds);
+ }
+
+ // Offset the sibling to our coordinates.
+ final int siblingDx = sibling.mLeft - mScrollX;
+ final int siblingDy = sibling.mTop - mScrollY;
+ siblingBounds.offset(siblingDx, siblingDy);
+
+ // Compute the intersection between the child and the sibling.
+ if (siblingBounds.intersect(bounds)) {
+ // If an interactive sibling completely covers the child, done.
+ if (siblingBounds.equals(bounds)) {
+ return false;
+ }
+ // Keep track of the intersection rectangle.
+ RectF intersection = new RectF(siblingBounds);
+ intersections.add(intersection);
+ }
+ }
+
+ if (mParent instanceof ViewGroup) {
+ ViewGroup parentGroup = (ViewGroup) mParent;
+ return parentGroup.translateBoundsAndIntersectionsInWindowCoordinates(
+ this, bounds, intersections);
+ }
+
+ return true;
+ }
+
+ /**
* Called when a child view has changed whether or not it is tracking transient state.
*/
public void childHasTransientStateChanged(View child, boolean childHasTransientState) {
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 4299e2e..80b9ade 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -6679,12 +6679,12 @@ public final class ViewRootImpl implements ViewParent,
public void performAccessibilityAction(long accessibilityNodeId, int action,
Bundle arguments, int interactionId,
IAccessibilityInteractionConnectionCallback callback, int flags,
- int interogatingPid, long interrogatingTid) {
+ int interrogatingPid, long interrogatingTid) {
ViewRootImpl viewRootImpl = mViewRootImpl.get();
if (viewRootImpl != null && viewRootImpl.mView != null) {
viewRootImpl.getAccessibilityInteractionController()
.performAccessibilityActionClientThread(accessibilityNodeId, action, arguments,
- interactionId, callback, flags, interogatingPid, interrogatingTid);
+ interactionId, callback, flags, interrogatingPid, interrogatingTid);
} else {
// We cannot make the call and notify the caller so it does not wait.
try {
@@ -6696,6 +6696,26 @@ public final class ViewRootImpl implements ViewParent,
}
@Override
+ public void computeClickPointInScreen(long accessibilityNodeId, Region interactiveRegion,
+ int interactionId, IAccessibilityInteractionConnectionCallback callback,
+ int interrogatingPid, long interrogatingTid, MagnificationSpec spec) {
+ ViewRootImpl viewRootImpl = mViewRootImpl.get();
+ if (viewRootImpl != null && viewRootImpl.mView != null) {
+ viewRootImpl.getAccessibilityInteractionController()
+ .computeClickPointInScreenClientThread(accessibilityNodeId,
+ interactiveRegion, interactionId, callback, interrogatingPid,
+ interrogatingTid, spec);
+ } else {
+ // We cannot make the call and notify the caller so it does not wait.
+ try {
+ callback.setComputeClickPointInScreenActionResult(null, interactionId);
+ } catch (RemoteException re) {
+ /* best effort - ignore */
+ }
+ }
+ }
+
+ @Override
public void findAccessibilityNodeInfosByViewId(long accessibilityNodeId,
String viewId, Region interactiveRegion, int interactionId,
IAccessibilityInteractionConnectionCallback callback, int flags,
diff --git a/core/java/android/view/accessibility/AccessibilityInteractionClient.java b/core/java/android/view/accessibility/AccessibilityInteractionClient.java
index db78ec5..374f7e0 100644
--- a/core/java/android/view/accessibility/AccessibilityInteractionClient.java
+++ b/core/java/android/view/accessibility/AccessibilityInteractionClient.java
@@ -17,6 +17,7 @@
package android.view.accessibility;
import android.accessibilityservice.IAccessibilityServiceConnection;
+import android.graphics.Point;
import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
@@ -98,6 +99,8 @@ public final class AccessibilityInteractionClient
private boolean mPerformAccessibilityActionResult;
+ private Point mComputeClickPointResult;
+
private Message mSameThreadMessage;
private static final SparseArray<IAccessibilityServiceConnection> sConnectionCache =
@@ -519,6 +522,43 @@ public final class AccessibilityInteractionClient
return false;
}
+ /**
+ * Computes a point in screen coordinates where sending a down/up events would
+ * perform a click on an {@link AccessibilityNodeInfo}.
+ *
+ * @param connectionId The id of a connection for interacting with the system.
+ * @param accessibilityWindowId A unique window id. Use
+ * {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID}
+ * to query the currently active window.
+ * @param accessibilityNodeId A unique view id or virtual descendant id from
+ * where to start the search. Use
+ * {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID}
+ * to start from the root.
+ * @return Point the click point of null if no such point.
+ */
+ public Point computeClickPointInScreen(int connectionId, int accessibilityWindowId,
+ long accessibilityNodeId) {
+ try {
+ IAccessibilityServiceConnection connection = getConnection(connectionId);
+ if (connection != null) {
+ final int interactionId = mInteractionIdCounter.getAndIncrement();
+ final boolean success = connection.computeClickPointInScreen(
+ accessibilityWindowId, accessibilityNodeId,
+ interactionId, this, Thread.currentThread().getId());
+ if (success) {
+ return getComputeClickPointInScreenResultAndClear(interactionId);
+ }
+ } else {
+ if (DEBUG) {
+ Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
+ }
+ }
+ } catch (RemoteException re) {
+ Log.w(LOG_TAG, "Error while calling remote computeClickPointInScreen", re);
+ }
+ return null;
+ }
+
public void clearCache() {
sAccessibilityCache.clear();
}
@@ -634,6 +674,34 @@ public final class AccessibilityInteractionClient
}
/**
+ * Gets the result of a request to compute a point in screen for clicking on a node.
+ *
+ * @param interactionId The interaction id to match the result with the request.
+ * @return The point or null if no such point.
+ */
+ private Point getComputeClickPointInScreenResultAndClear(int interactionId) {
+ synchronized (mInstanceLock) {
+ final boolean success = waitForResultTimedLocked(interactionId);
+ Point result = success ? mComputeClickPointResult : null;
+ clearResultLocked();
+ return result;
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void setComputeClickPointInScreenActionResult(Point point, int interactionId) {
+ synchronized (mInstanceLock) {
+ if (interactionId > mInteractionId) {
+ mComputeClickPointResult = point;
+ mInteractionId = interactionId;
+ }
+ mInstanceLock.notifyAll();
+ }
+ }
+
+ /**
* Clears the result state.
*/
private void clearResultLocked() {
@@ -641,6 +709,7 @@ public final class AccessibilityInteractionClient
mFindAccessibilityNodeInfoResult = null;
mFindAccessibilityNodeInfosResult = null;
mPerformAccessibilityActionResult = false;
+ mComputeClickPointResult = null;
}
/**
diff --git a/core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl b/core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl
index faf7789..66a3f46 100644
--- a/core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl
+++ b/core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl
@@ -17,6 +17,7 @@
package android.view.accessibility;
import android.graphics.Region;
+import android.graphics.Point;
import android.os.Bundle;
import android.view.MagnificationSpec;
import android.view.accessibility.AccessibilityNodeInfo;
@@ -53,4 +54,8 @@ oneway interface IAccessibilityInteractionConnection {
void performAccessibilityAction(long accessibilityNodeId, int action, in Bundle arguments,
int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags,
int interrogatingPid, long interrogatingTid);
+
+ void computeClickPointInScreen(long accessibilityNodeId, in Region bounds, int interactionId,
+ IAccessibilityInteractionConnectionCallback callback, int interrogatingPid,
+ long interrogatingTid, in MagnificationSpec spec);
}
diff --git a/core/java/android/view/accessibility/IAccessibilityInteractionConnectionCallback.aidl b/core/java/android/view/accessibility/IAccessibilityInteractionConnectionCallback.aidl
index c1a3ab7..f480216 100644
--- a/core/java/android/view/accessibility/IAccessibilityInteractionConnectionCallback.aidl
+++ b/core/java/android/view/accessibility/IAccessibilityInteractionConnectionCallback.aidl
@@ -16,6 +16,7 @@
package android.view.accessibility;
+import android.graphics.Point;
import android.view.accessibility.AccessibilityNodeInfo;
import java.util.List;
@@ -51,4 +52,12 @@ oneway interface IAccessibilityInteractionConnectionCallback {
* @param interactionId The interaction id to match the result with the request.
*/
void setPerformAccessibilityActionResult(boolean succeeded, int interactionId);
+
+ /**
+ * Sets the result of a request to compute a point for clicking in a view.
+ *
+ * @param point The point of null if no such point.
+ * @param interactionId The interaction id to match the result with the request.
+ */
+ void setComputeClickPointInScreenActionResult(in Point point, int interactionId);
}
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index a43a2a6..ebe21ff 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -746,14 +746,16 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub {
}
/**
- * Gets the bounds of the accessibility focus in the active window.
+ * Gets a point within the accessibility focused node where we can send down
+ * and up events to perform a click.
*
- * @param outBounds The output to which to write the focus bounds.
- * @return Whether accessibility focus was found and the bounds are populated.
+ * @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 getAccessibilityFocusBounds(Rect outBounds) {
- return getInteractionBridgeLocked().getAccessibilityFocusBoundsNotLocked(outBounds);
+ boolean getAccessibilityFocusClickPointInScreen(Point outPoint) {
+ return getInteractionBridgeLocked()
+ .getAccessibilityFocusClickPointInScreenNotLocked(outPoint);
}
/**
@@ -2196,8 +2198,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub {
MagnificationSpec spec = getCompatibleMagnificationSpecLocked(resolvedWindowId);
try {
connection.findAccessibilityNodeInfosByViewId(accessibilityNodeId, viewIdResName,
- partialInteractiveRegion, interactionId, callback, mFetchFlags, interrogatingPid,
- interrogatingTid, spec);
+ partialInteractiveRegion, interactionId, callback, mFetchFlags,
+ interrogatingPid, interrogatingTid, spec);
return true;
} catch (RemoteException re) {
if (DEBUG) {
@@ -2248,8 +2250,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub {
MagnificationSpec spec = getCompatibleMagnificationSpecLocked(resolvedWindowId);
try {
connection.findAccessibilityNodeInfosByText(accessibilityNodeId, text,
- partialInteractiveRegion, interactionId, callback, mFetchFlags, interrogatingPid,
- interrogatingTid, spec);
+ partialInteractiveRegion, interactionId, callback, mFetchFlags,
+ interrogatingPid, interrogatingTid, spec);
return true;
} catch (RemoteException re) {
if (DEBUG) {
@@ -2352,8 +2354,9 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub {
final long identityToken = Binder.clearCallingIdentity();
MagnificationSpec spec = getCompatibleMagnificationSpecLocked(resolvedWindowId);
try {
- connection.findFocus(accessibilityNodeId, focusType, partialInteractiveRegion, interactionId,
- callback, mFetchFlags, interrogatingPid, interrogatingTid, spec);
+ connection.findFocus(accessibilityNodeId, focusType, partialInteractiveRegion,
+ interactionId, callback, mFetchFlags, interrogatingPid, interrogatingTid,
+ spec);
return true;
} catch (RemoteException re) {
if (DEBUG) {
@@ -2403,8 +2406,9 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub {
final long identityToken = Binder.clearCallingIdentity();
MagnificationSpec spec = getCompatibleMagnificationSpecLocked(resolvedWindowId);
try {
- connection.focusSearch(accessibilityNodeId, direction, partialInteractiveRegion, interactionId,
- callback, mFetchFlags, interrogatingPid, interrogatingTid, spec);
+ connection.focusSearch(accessibilityNodeId, direction, partialInteractiveRegion,
+ interactionId, callback, mFetchFlags, interrogatingPid, interrogatingTid,
+ spec);
return true;
} catch (RemoteException re) {
if (DEBUG) {
@@ -2460,6 +2464,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub {
return true;
}
+ @Override
public boolean performGlobalAction(int action) {
synchronized (mLock) {
// We treat calls from a profile as if made by its parent as profiles
@@ -2501,6 +2506,57 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub {
}
@Override
+ public boolean computeClickPointInScreen(int accessibilityWindowId,
+ long accessibilityNodeId, int interactionId,
+ IAccessibilityInteractionConnectionCallback callback, long interrogatingTid)
+ throws RemoteException {
+ final int resolvedWindowId;
+ IAccessibilityInteractionConnection connection = null;
+ Region partialInteractiveRegion = mTempRegion;
+ synchronized (mLock) {
+ // We treat calls from a profile as if made by its parent as profiles
+ // share the accessibility state of the parent. The call below
+ // performs the current profile parent resolution.
+ final int resolvedUserId = mSecurityPolicy
+ .resolveCallingUserIdEnforcingPermissionsLocked(
+ UserHandle.getCallingUserId());
+ if (resolvedUserId != mCurrentUserId) {
+ return false;
+ }
+ resolvedWindowId = resolveAccessibilityWindowIdLocked(accessibilityWindowId);
+ final boolean permissionGranted =
+ mSecurityPolicy.canRetrieveWindowContentLocked(this);
+ if (!permissionGranted) {
+ return false;
+ } else {
+ connection = getConnectionLocked(resolvedWindowId);
+ if (connection == null) {
+ return false;
+ }
+ }
+ if (!mSecurityPolicy.computePartialInteractiveRegionForWindowLocked(
+ resolvedWindowId, partialInteractiveRegion)) {
+ partialInteractiveRegion = null;
+ }
+ }
+ final int interrogatingPid = Binder.getCallingPid();
+ final long identityToken = Binder.clearCallingIdentity();
+ MagnificationSpec spec = getCompatibleMagnificationSpecLocked(resolvedWindowId);
+ try {
+ connection.computeClickPointInScreen(accessibilityNodeId, partialInteractiveRegion,
+ interactionId, callback, interrogatingPid, interrogatingTid, spec);
+ return true;
+ } catch (RemoteException re) {
+ if (DEBUG) {
+ Slog.e(LOG_TAG, "Error computeClickPointInScreen().");
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identityToken);
+ }
+ return false;
+ }
+
+ @Override
public void dump(FileDescriptor fd, final PrintWriter pw, String[] args) {
mSecurityPolicy.enforceCallingPermission(Manifest.permission.DUMP, FUNCTION_DUMP);
synchronized (mLock) {
@@ -3119,30 +3175,43 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub {
}
}
- public boolean getAccessibilityFocusBoundsNotLocked(Rect outBounds) {
+ public boolean getAccessibilityFocusClickPointInScreenNotLocked(Point outPoint) {
AccessibilityNodeInfo focus = getAccessibilityFocusNotLocked();
if (focus == null) {
return false;
}
synchronized (mLock) {
- focus.getBoundsInScreen(outBounds);
+ Point point = mClient.computeClickPointInScreen(mConnectionId,
+ focus.getWindowId(), focus.getSourceNodeId());
+
+ if (point == null) {
+ return false;
+ }
MagnificationSpec spec = getCompatibleMagnificationSpecLocked(focus.getWindowId());
if (spec != null && !spec.isNop()) {
- outBounds.offset((int) -spec.offsetX, (int) -spec.offsetY);
- outBounds.scale(1 / spec.scale);
+ point.offset((int) -spec.offsetX, (int) -spec.offsetY);
+ point.x = (int) (point.x * (1 / spec.scale));
+ point.y = (int) (point.y * (1 / spec.scale));
}
- // Clip to the window rectangle.
+ // Make sure the point is within the window.
Rect windowBounds = mTempRect;
getActiveWindowBounds(windowBounds);
- outBounds.intersect(windowBounds);
+ if (!windowBounds.contains(point.x, point.y)) {
+ return false;
+ }
- // Clip to the screen rectangle.
- mDefaultDisplay.getRealSize(mTempPoint);
- outBounds.intersect(0, 0, mTempPoint.x, mTempPoint.y);
+ // Make sure the point is within the screen.
+ Point screenSize = mTempPoint;
+ mDefaultDisplay.getRealSize(screenSize);
+ if (point.x < 0 || point.x > screenSize.x
+ || point.y < 0 || point.y > screenSize.y) {
+ return false;
+ }
+ outPoint.set(point.x, point.y);
return true;
}
}
diff --git a/services/accessibility/java/com/android/server/accessibility/TouchExplorer.java b/services/accessibility/java/com/android/server/accessibility/TouchExplorer.java
index ac0ca0a..9e63433 100644
--- a/services/accessibility/java/com/android/server/accessibility/TouchExplorer.java
+++ b/services/accessibility/java/com/android/server/accessibility/TouchExplorer.java
@@ -24,6 +24,7 @@ import android.gesture.GesturePoint;
import android.gesture.GestureStore;
import android.gesture.GestureStroke;
import android.gesture.Prediction;
+import android.graphics.Point;
import android.graphics.Rect;
import android.os.Handler;
import android.os.SystemClock;
@@ -171,6 +172,9 @@ class TouchExplorer implements EventStreamTransformation {
// Temporary rectangle to avoid instantiation.
private final Rect mTempRect = new Rect();
+ // Temporary point to avoid instantiation.
+ private final Point mTempPoint = new Point();
+
// Context in which this explorer operates.
private final Context mContext;
@@ -1157,18 +1161,18 @@ class TouchExplorer implements EventStreamTransformation {
mInjectedPointerTracker.getLastInjectedHoverEventForClick();
if (lastExploreEvent == null) {
// No last touch explored event but there is accessibility focus in
- // the active window. We click in the middle of the focus bounds.
- Rect focusBounds = mTempRect;
- if (mAms.getAccessibilityFocusBounds(focusBounds)) {
- clickLocationX = focusBounds.centerX();
- clickLocationY = focusBounds.centerY();
+ // the active window. We click in the focus bounds.
+ Point point = mTempPoint;
+ if (mAms.getAccessibilityFocusClickPointInScreen(point)) {
+ clickLocationX = point.x;
+ clickLocationY = point.y;
} else {
// Out of luck - do nothing.
return;
}
} else {
// If the click is within the active window but not within the
- // accessibility focus bounds we click in the focus center.
+ // accessibility focus bounds we click in the focus bounds.
final int lastExplorePointerIndex = lastExploreEvent.getActionIndex();
clickLocationX = (int) lastExploreEvent.getX(lastExplorePointerIndex);
clickLocationY = (int) lastExploreEvent.getY(lastExplorePointerIndex);
@@ -1176,12 +1180,10 @@ class TouchExplorer implements EventStreamTransformation {
if (mLastTouchedWindowId == mAms.getActiveWindowId()) {
mAms.getActiveWindowBounds(activeWindowBounds);
if (activeWindowBounds.contains(clickLocationX, clickLocationY)) {
- Rect focusBounds = mTempRect;
- if (mAms.getAccessibilityFocusBounds(focusBounds)) {
- if (!focusBounds.contains(clickLocationX, clickLocationY)) {
- clickLocationX = focusBounds.centerX();
- clickLocationY = focusBounds.centerY();
- }
+ Point point = mTempPoint;
+ if (mAms.getAccessibilityFocusClickPointInScreen(point)) {
+ clickLocationX = point.x;
+ clickLocationY = point.y;
}
}
}
@@ -1330,18 +1332,18 @@ class TouchExplorer implements EventStreamTransformation {
mInjectedPointerTracker.getLastInjectedHoverEventForClick();
if (lastExploreEvent == null) {
// No last touch explored event but there is accessibility focus in
- // the active window. We click in the middle of the focus bounds.
- Rect focusBounds = mTempRect;
- if (mAms.getAccessibilityFocusBounds(focusBounds)) {
- clickLocationX = focusBounds.centerX();
- clickLocationY = focusBounds.centerY();
+ // the active window. We click in the focus bounds.
+ Point point = mTempPoint;
+ if (mAms.getAccessibilityFocusClickPointInScreen(point)) {
+ clickLocationX = point.x;
+ clickLocationY = point.y;
} else {
// Out of luck - do nothing.
return;
}
} else {
// If the click is within the active window but not within the
- // accessibility focus bounds we click in the focus center.
+ // accessibility focus bounds we click in the focus bounds.
final int lastExplorePointerIndex = lastExploreEvent.getActionIndex();
clickLocationX = (int) lastExploreEvent.getX(lastExplorePointerIndex);
clickLocationY = (int) lastExploreEvent.getY(lastExplorePointerIndex);
@@ -1349,12 +1351,10 @@ class TouchExplorer implements EventStreamTransformation {
if (mLastTouchedWindowId == mAms.getActiveWindowId()) {
mAms.getActiveWindowBounds(activeWindowBounds);
if (activeWindowBounds.contains(clickLocationX, clickLocationY)) {
- Rect focusBounds = mTempRect;
- if (mAms.getAccessibilityFocusBounds(focusBounds)) {
- if (!focusBounds.contains(clickLocationX, clickLocationY)) {
- clickLocationX = focusBounds.centerX();
- clickLocationY = focusBounds.centerY();
- }
+ Point point = mTempPoint;
+ if (mAms.getAccessibilityFocusClickPointInScreen(point)) {
+ clickLocationX = point.x;
+ clickLocationY = point.y;
}
}
}