diff options
Diffstat (limited to 'services/java/com/android/server')
4 files changed, 947 insertions, 447 deletions
diff --git a/services/java/com/android/server/accessibility/AccessibilityInputFilter.java b/services/java/com/android/server/accessibility/AccessibilityInputFilter.java index 3fbac38..3e35b20 100644 --- a/services/java/com/android/server/accessibility/AccessibilityInputFilter.java +++ b/services/java/com/android/server/accessibility/AccessibilityInputFilter.java @@ -16,7 +16,6 @@ package com.android.server.accessibility; -import com.android.server.accessibility.TouchExplorer.GestureListener; import com.android.server.input.InputFilter; import android.content.Context; @@ -40,7 +39,7 @@ public class AccessibilityInputFilter extends InputFilter { private final PowerManager mPm; - private final GestureListener mGestureListener; + private final AccessibilityManagerService mAms; /** * This is an interface for explorers that take a {@link MotionEvent} @@ -73,10 +72,10 @@ public class AccessibilityInputFilter extends InputFilter { private int mTouchscreenSourceDeviceId; - public AccessibilityInputFilter(Context context, GestureListener gestureListener) { + public AccessibilityInputFilter(Context context, AccessibilityManagerService service) { super(context.getMainLooper()); mContext = context; - mGestureListener = gestureListener; + mAms = service; mPm = (PowerManager)context.getSystemService(Context.POWER_SERVICE); } @@ -85,7 +84,7 @@ public class AccessibilityInputFilter extends InputFilter { if (DEBUG) { Slog.d(TAG, "Accessibility input filter installed."); } - mTouchExplorer = new TouchExplorer(this, mContext, mGestureListener); + mTouchExplorer = new TouchExplorer(this, mContext, mAms); super.onInstalled(); } diff --git a/services/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/java/com/android/server/accessibility/AccessibilityManagerService.java index f23b25e..ebc2074 100644 --- a/services/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -24,12 +24,15 @@ import android.accessibilityservice.AccessibilityService; import android.accessibilityservice.AccessibilityServiceInfo; import android.accessibilityservice.IAccessibilityServiceClient; import android.accessibilityservice.IAccessibilityServiceConnection; +import android.app.AlertDialog; import android.app.PendingIntent; import android.app.StatusBarManager; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; import android.content.Intent; import android.content.IntentFilter; import android.content.ServiceConnection; @@ -58,7 +61,9 @@ import android.view.IWindow; import android.view.InputDevice; import android.view.KeyCharacterMap; import android.view.KeyEvent; +import android.view.WindowManager; import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityInteractionClient; import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.IAccessibilityInteractionConnection; @@ -66,8 +71,8 @@ import android.view.accessibility.IAccessibilityInteractionConnectionCallback; import android.view.accessibility.IAccessibilityManager; import android.view.accessibility.IAccessibilityManagerClient; +import com.android.internal.R; import com.android.internal.content.PackageMonitor; -import com.android.server.accessibility.TouchExplorer.GestureListener; import com.android.server.wm.WindowManagerService; import org.xmlpull.v1.XmlPullParserException; @@ -90,8 +95,7 @@ import java.util.Set; * * @hide */ -public class AccessibilityManagerService extends IAccessibilityManager.Stub - implements GestureListener { +public class AccessibilityManagerService extends IAccessibilityManager.Stub { private static final boolean DEBUG = false; @@ -102,6 +106,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub private static final int OWN_PROCESS_ID = android.os.Process.myPid(); + private static final int MSG_SHOW_ENABLE_TOUCH_EXPLORATION_DIALOG = 1; + private static int sIdCounter = 0; private static int sNextWindowId; @@ -128,6 +134,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub private final SimpleStringSplitter mStringColonSplitter = new SimpleStringSplitter(':'); + private final Rect mTempRect = new Rect(); + private PackageManager mPackageManager; private int mHandledFeedbackTypes = 0; @@ -146,23 +154,15 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub private final SecurityPolicy mSecurityPolicy; + private final MainHanler mMainHandler; + private Service mUiAutomationService; - /** - * Handler for delayed event dispatch. - */ - private Handler mHandler = new Handler() { + private Service mQueryBridge; - @Override - public void handleMessage(Message message) { - Service service = (Service) message.obj; - int eventType = message.arg1; + private boolean mTouchExplorationGestureEnded; - synchronized (mLock) { - notifyAccessibilityEventLocked(service, eventType); - } - } - }; + private boolean mTouchExplorationGestureStarted; /** * Creates a new instance. @@ -175,7 +175,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub mWindowManagerService = (WindowManagerService) ServiceManager.getService( Context.WINDOW_SERVICE); mSecurityPolicy = new SecurityPolicy(); - + mMainHandler = new MainHanler(); registerPackageChangeAndBootCompletedBroadcastReceiver(); registerSettingsContentObservers(); } @@ -349,15 +349,37 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } public boolean sendAccessibilityEvent(AccessibilityEvent event) { + // The event for gesture start should be strictly before the + // first hover enter event for the gesture. + if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_HOVER_ENTER + && mTouchExplorationGestureStarted) { + mTouchExplorationGestureStarted = false; + AccessibilityEvent gestureStartEvent = AccessibilityEvent.obtain( + AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_START); + sendAccessibilityEvent(gestureStartEvent); + } + synchronized (mLock) { if (mSecurityPolicy.canDispatchAccessibilityEvent(event)) { - mSecurityPolicy.updateRetrievalAllowingWindowAndEventSourceLocked(event); + mSecurityPolicy.updateActiveWindowAndEventSourceLocked(event); notifyAccessibilityServicesDelayedLocked(event, false); notifyAccessibilityServicesDelayedLocked(event, true); } } + + // The event for gesture end should be strictly after the + // last hover exit event for the gesture. + if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_HOVER_EXIT + && mTouchExplorationGestureEnded) { + mTouchExplorationGestureEnded = false; + AccessibilityEvent gestureEndEvent = AccessibilityEvent.obtain( + AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_END); + sendAccessibilityEvent(gestureEndEvent); + } + event.recycle(); mHandledFeedbackTypes = 0; + return (OWN_PROCESS_ID != Binder.getCallingPid()); } @@ -472,8 +494,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } } - @Override - public boolean onGesture(int gestureId) { + boolean onGesture(int gestureId) { synchronized (mLock) { boolean handled = notifyGestureLocked(gestureId, false); if (!handled) { @@ -483,6 +504,65 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } } + /** + * Gets the bounds of the accessibility focus if the provided, + * point coordinates are within the currently active window + * and accessibility focus is found within the latter. + * + * @param x X coordinate. + * @param y Y coordinate + * @param outBounds The output to which to write the focus bounds. + * @return The accessibility focus rectangle if the point is in the + * window and the window has accessibility focus. + */ + boolean getAccessibilityFocusBounds(int x, int y, Rect outBounds) { + // Instead of keeping track of accessibility focus events per + // window to be able to find the focus in the active window, + // we take a stateless approach and look it up. This is fine + // since we do this only when the user clicks/long presses. + Service service = getQueryBridge(); + final int connectionId = service.mId; + AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance(); + client.addConnection(connectionId, service); + try { + AccessibilityNodeInfo root = AccessibilityInteractionClient.getInstance() + .getRootInActiveWindow(connectionId); + if (root == null) { + return false; + } + Rect bounds = mTempRect; + root.getBoundsInScreen(bounds); + if (!bounds.contains(x, y)) { + return false; + } + AccessibilityNodeInfo focus = root.findFocus( + AccessibilityNodeInfo.FOCUS_ACCESSIBILITY); + if (focus == null) { + return false; + } + focus.getBoundsInScreen(outBounds); + return true; + } finally { + client.removeConnection(connectionId); + } + } + + private Service getQueryBridge() { + if (mQueryBridge == null) { + AccessibilityServiceInfo info = new AccessibilityServiceInfo(); + mQueryBridge = new Service(null, info, true); + } + return mQueryBridge; + } + + public void touchExplorationGestureEnded() { + mTouchExplorationGestureEnded = true; + } + + public void touchExplorationGestureStarted() { + mTouchExplorationGestureStarted = true; + } + private boolean notifyGestureLocked(int gestureId, boolean isDefault) { // TODO: Now we are giving the gestures to the last enabled // service that can handle them which is the last one @@ -496,12 +576,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub for (int i = mServices.size() - 1; i >= 0; i--) { Service service = mServices.get(i); if (service.mReqeustTouchExplorationMode && service.mIsDefault == isDefault) { - try { - service.mServiceInterface.onGesture(gestureId); - } catch (RemoteException re) { - Slog.e(LOG_TAG, "Error during sending gesture " + gestureId - + " to " + service.mService, re); - } + service.notifyGesture(gestureId); return true; } } @@ -573,7 +648,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub if (service.mIsDefault == isDefault) { if (canDispathEventLocked(service, event, mHandledFeedbackTypes)) { mHandledFeedbackTypes |= service.mFeedbackType; - notifyAccessibilityServiceDelayedLocked(service, event); + service.notifyAccessibilityEvent(event); } } } @@ -586,90 +661,6 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } /** - * Performs an {@link AccessibilityService} delayed notification. The delay is configurable - * and denotes the period after the last event before notifying the service. - * - * @param service The service. - * @param event The event. - */ - private void notifyAccessibilityServiceDelayedLocked(Service service, - AccessibilityEvent event) { - synchronized (mLock) { - final int eventType = event.getEventType(); - // Make a copy since during dispatch it is possible the event to - // be modified to remove its source if the receiving service does - // not have permission to access the window content. - AccessibilityEvent newEvent = AccessibilityEvent.obtain(event); - AccessibilityEvent oldEvent = service.mPendingEvents.get(eventType); - service.mPendingEvents.put(eventType, newEvent); - - final int what = eventType | (service.mId << 16); - if (oldEvent != null) { - mHandler.removeMessages(what); - oldEvent.recycle(); - } - - Message message = mHandler.obtainMessage(what, service); - message.arg1 = eventType; - mHandler.sendMessageDelayed(message, service.mNotificationTimeout); - } - } - - /** - * Notifies an accessibility service client for a scheduled event given the event type. - * - * @param service The service client. - * @param eventType The type of the event to dispatch. - */ - private void notifyAccessibilityEventLocked(Service service, int eventType) { - IAccessibilityServiceClient listener = service.mServiceInterface; - - // If the service died/was disabled while the message for dispatching - // the accessibility event was propagating the listener may be null. - if (listener == null) { - return; - } - - AccessibilityEvent event = service.mPendingEvents.get(eventType); - - // Check for null here because there is a concurrent scenario in which this - // happens: 1) A binder thread calls notifyAccessibilityServiceDelayedLocked - // which posts a message for dispatching an event. 2) The message is pulled - // from the queue by the handler on the service thread and the latter is - // just about to acquire the lock and call this method. 3) Now another binder - // thread acquires the lock calling notifyAccessibilityServiceDelayedLocked - // so the service thread waits for the lock; 4) The binder thread replaces - // the event with a more recent one (assume the same event type) and posts a - // dispatch request releasing the lock. 5) Now the main thread is unblocked and - // dispatches the event which is removed from the pending ones. 6) And ... now - // the service thread handles the last message posted by the last binder call - // but the event is already dispatched and hence looking it up in the pending - // ones yields null. This check is much simpler that keeping count for each - // event type of each service to catch such a scenario since only one message - // is processed at a time. - if (event == null) { - return; - } - - service.mPendingEvents.remove(eventType); - try { - if (mSecurityPolicy.canRetrieveWindowContent(service)) { - event.setConnectionId(service.mId); - } else { - event.setSource(null); - } - event.setSealed(true); - listener.onAccessibilityEvent(event); - event.recycle(); - if (DEBUG) { - Slog.i(LOG_TAG, "Event " + event + " sent to " + listener); - } - } catch (RemoteException re) { - Slog.e(LOG_TAG, "Error during sending " + event + " to " + service.mService, re); - } - } - - /** * Adds a service. * * @param service The service to add. @@ -683,6 +674,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub mServices.add(service); mComponentNameToServiceMap.put(service.mComponentName, service); updateInputFilterLocked(); + tryEnableTouchExploration(service); } catch (RemoteException e) { /* do nothing */ } @@ -700,10 +692,10 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub return false; } mComponentNameToServiceMap.remove(service.mComponentName); - mHandler.removeMessages(service.mId); service.unlinkToOwnDeath(); service.dispose(); updateInputFilterLocked(); + tryDisableTouchExploration(service); return removed; } @@ -932,6 +924,29 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub Settings.Secure.TOUCH_EXPLORATION_ENABLED, 0) == 1; } + private void tryEnableTouchExploration(final Service service) { + if (!mIsTouchExplorationEnabled && service.mRequestTouchExplorationMode) { + mMainHandler.obtainMessage(MSG_SHOW_ENABLE_TOUCH_EXPLORATION_DIALOG, + service).sendToTarget(); + } + } + + private void tryDisableTouchExploration(Service service) { + if (mIsTouchExplorationEnabled && service.mReqeustTouchExplorationMode) { + synchronized (mLock) { + final int serviceCount = mServices.size(); + for (int i = 0; i < serviceCount; i++) { + Service other = mServices.get(i); + if (other != service && other.mRequestTouchExplorationMode) { + return; + } + } + Settings.Secure.putInt(mContext.getContentResolver(), + Settings.Secure.TOUCH_EXPLORATION_ENABLED, 0); + } + } + } + private class AccessibilityConnectionWrapper implements DeathRecipient { private final int mWindowId; private final IAccessibilityInteractionConnection mConnection; @@ -959,6 +974,42 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } } + private class MainHanler extends Handler { + @Override + public void handleMessage(Message msg) { + final int type = msg.what; + switch (type) { + case MSG_SHOW_ENABLE_TOUCH_EXPLORATION_DIALOG: { + Service service = (Service) msg.obj; + String label = service.mResolveInfo.loadLabel( + mContext.getPackageManager()).toString(); + final AlertDialog dialog = new AlertDialog.Builder(mContext) + .setIcon(android.R.drawable.ic_dialog_alert) + .setPositiveButton(android.R.string.ok, new OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Settings.Secure.putInt(mContext.getContentResolver(), + Settings.Secure.TOUCH_EXPLORATION_ENABLED, 1); + } + }) + .setNegativeButton(android.R.string.cancel, new OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + } + }) + .setTitle(R.string.enable_explore_by_touch_warning_title) + .setMessage(mContext.getString( + R.string.enable_explore_by_touch_warning_message, label)) + .create(); + dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_INPUT_METHOD_DIALOG); + dialog.setCanceledOnTouchOutside(true); + dialog.show(); + } + } + } + } + /** * This class represents an accessibility service. It stores all per service * data required for the service management, provides API for starting/stopping the @@ -969,6 +1020,11 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub */ class Service extends IAccessibilityServiceConnection.Stub implements ServiceConnection, DeathRecipient { + + // We pick the MSB to avoid collision since accessibility event types are + // used as message types allowing us to remove messages per event type. + private static final int MSG_ON_GESTURE = 0x80000000; + int mId = 0; AccessibilityServiceInfo mAccessibilityServiceInfo; @@ -985,6 +1041,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub boolean mIsDefault; + boolean mRequestTouchExplorationMode; + boolean mIncludeNotImportantViews; long mNotificationTimeout; @@ -1001,12 +1059,35 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub final Rect mTempBounds = new Rect(); + final ResolveInfo mResolveInfo; + // the events pending events to be dispatched to this service final SparseArray<AccessibilityEvent> mPendingEvents = new SparseArray<AccessibilityEvent>(); + /** + * Handler for delayed event dispatch. + */ + public Handler mHandler = new Handler() { + @Override + public void handleMessage(Message message) { + final int type = message.what; + switch (type) { + case MSG_ON_GESTURE: { + final int gestureId = message.arg1; + notifyGestureInternal(gestureId); + } break; + default: { + final int eventType = type; + notifyAccessibilityEventInternal(eventType); + } break; + } + } + }; + public Service(ComponentName componentName, AccessibilityServiceInfo accessibilityServiceInfo, boolean isAutomation) { + mResolveInfo = accessibilityServiceInfo.getResolveInfo(); mId = sIdCounter++; mComponentName = componentName; mAccessibilityServiceInfo = accessibilityServiceInfo; @@ -1043,6 +1124,9 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub (info.flags & FLAG_INCLUDE_NOT_IMPORTANT_VIEWS) != 0; } + mRequestTouchExplorationMode = (info.flags + & AccessibilityServiceInfo.FLAG_REQUEST_TOUCH_EXPLORATION_MODE) != 0; + synchronized (mLock) { tryAddServiceLocked(this); } @@ -1403,6 +1487,108 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } } + /** + * Performs a notification for an {@link AccessibilityEvent}. + * + * @param event The event. + */ + public void notifyAccessibilityEvent(AccessibilityEvent event) { + synchronized (mLock) { + final int eventType = event.getEventType(); + // Make a copy since during dispatch it is possible the event to + // be modified to remove its source if the receiving service does + // not have permission to access the window content. + AccessibilityEvent newEvent = AccessibilityEvent.obtain(event); + AccessibilityEvent oldEvent = mPendingEvents.get(eventType); + mPendingEvents.put(eventType, newEvent); + + final int what = eventType; + if (oldEvent != null) { + mHandler.removeMessages(what); + oldEvent.recycle(); + } + + Message message = mHandler.obtainMessage(what); + mHandler.sendMessageDelayed(message, mNotificationTimeout); + } + } + + /** + * Notifies an accessibility service client for a scheduled event given the event type. + * + * @param eventType The type of the event to dispatch. + */ + private void notifyAccessibilityEventInternal(int eventType) { + IAccessibilityServiceClient listener; + AccessibilityEvent event; + + synchronized (mLock) { + listener = mServiceInterface; + + // If the service died/was disabled while the message for dispatching + // the accessibility event was propagating the listener may be null. + if (listener == null) { + return; + } + + event = mPendingEvents.get(eventType); + + // Check for null here because there is a concurrent scenario in which this + // happens: 1) A binder thread calls notifyAccessibilityServiceDelayedLocked + // which posts a message for dispatching an event. 2) The message is pulled + // from the queue by the handler on the service thread and the latter is + // just about to acquire the lock and call this method. 3) Now another binder + // thread acquires the lock calling notifyAccessibilityServiceDelayedLocked + // so the service thread waits for the lock; 4) The binder thread replaces + // the event with a more recent one (assume the same event type) and posts a + // dispatch request releasing the lock. 5) Now the main thread is unblocked and + // dispatches the event which is removed from the pending ones. 6) And ... now + // the service thread handles the last message posted by the last binder call + // but the event is already dispatched and hence looking it up in the pending + // ones yields null. This check is much simpler that keeping count for each + // event type of each service to catch such a scenario since only one message + // is processed at a time. + if (event == null) { + return; + } + + mPendingEvents.remove(eventType); + if (mSecurityPolicy.canRetrieveWindowContent(this)) { + event.setConnectionId(mId); + } else { + event.setSource(null); + } + event.setSealed(true); + } + + try { + listener.onAccessibilityEvent(event); + if (DEBUG) { + Slog.i(LOG_TAG, "Event " + event + " sent to " + listener); + } + } catch (RemoteException re) { + Slog.e(LOG_TAG, "Error during sending " + event + " to " + listener, re); + } finally { + event.recycle(); + } + } + + public void notifyGesture(int gestureId) { + mHandler.obtainMessage(MSG_ON_GESTURE, gestureId, 0).sendToTarget(); + } + + private void notifyGestureInternal(int gestureId) { + IAccessibilityServiceClient listener = mServiceInterface; + if (listener != null) { + try { + listener.onGesture(gestureId); + } catch (RemoteException re) { + Slog.e(LOG_TAG, "Error during sending gesture " + gestureId + + " to " + mService, re); + } + } + } + private void sendDownAndUpKeyEvents(int keyCode) { final long token = Binder.clearCallingIdentity(); @@ -1454,7 +1640,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub private int resolveAccessibilityWindowId(int accessibilityWindowId) { if (accessibilityWindowId == AccessibilityNodeInfo.ACTIVE_WINDOW_ID) { - return mSecurityPolicy.mRetrievalAlowingWindowId; + return mSecurityPolicy.mActiveWindowId; } return accessibilityWindowId; } @@ -1497,24 +1683,35 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub | AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED | AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED; - private static final int RETRIEVAL_ALLOWING_WINDOW_CHANGE_EVENT_TYPES = - AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED - | AccessibilityEvent.TYPE_VIEW_HOVER_ENTER - | AccessibilityEvent.TYPE_VIEW_HOVER_EXIT; - - private int mRetrievalAlowingWindowId; + private int mActiveWindowId; private boolean canDispatchAccessibilityEvent(AccessibilityEvent event) { // Send window changed event only for the retrieval allowing window. return (event.getEventType() != AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED - || event.getWindowId() == mRetrievalAlowingWindowId); + || event.getWindowId() == mActiveWindowId); } - public void updateRetrievalAllowingWindowAndEventSourceLocked(AccessibilityEvent event) { + public void updateActiveWindowAndEventSourceLocked(AccessibilityEvent event) { + // The active window is either the window that has input focus or + // the window that the user is currently touching. If the user is + // touching a window that does not have input focus as soon as the + // the user stops touching that window the focused window becomes + // the active one. final int windowId = event.getWindowId(); final int eventType = event.getEventType(); - if ((eventType & RETRIEVAL_ALLOWING_WINDOW_CHANGE_EVENT_TYPES) != 0) { - mRetrievalAlowingWindowId = windowId; + switch (eventType) { + case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED: { + if (getFocusedWindowId() == windowId) { + mActiveWindowId = windowId; + } + } break; + case AccessibilityEvent.TYPE_VIEW_HOVER_ENTER: + case AccessibilityEvent.TYPE_VIEW_HOVER_EXIT: { + mActiveWindowId = windowId; + } break; + case AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_END: { + mActiveWindowId = getFocusedWindowId(); + } break; } if ((eventType & RETRIEVAL_ALLOWING_EVENT_TYPES) == 0) { event.setSource(null); @@ -1522,7 +1719,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } public int getRetrievalAllowingWindowLocked() { - return mRetrievalAlowingWindowId; + return mActiveWindowId; } public boolean canGetAccessibilityNodeInfoLocked(Service service, int windowId) { @@ -1550,7 +1747,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } private boolean isRetrievalAllowingWindow(int windowId) { - return (mRetrievalAlowingWindowId == windowId); + return (mActiveWindowId == windowId); } private boolean isActionPermitted(int action) { @@ -1567,5 +1764,22 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub + " required to call " + function); } } + + private int getFocusedWindowId() { + // We call this only on window focus change or after touch + // exploration gesture end and the shown windows are not that + // many, so the linear look up is just fine. + IBinder token = mWindowManagerService.getFocusedWindowClientToken(); + if (token != null) { + SparseArray<IBinder> windows = mWindowIdToWindowTokenMap; + final int windowCount = windows.size(); + for (int i = 0; i < windowCount; i++) { + if (windows.valueAt(i) == token) { + return windows.keyAt(i); + } + } + } + return -1; + } } } diff --git a/services/java/com/android/server/accessibility/TouchExplorer.java b/services/java/com/android/server/accessibility/TouchExplorer.java index 39012e6..b0b2b8d 100644 --- a/services/java/com/android/server/accessibility/TouchExplorer.java +++ b/services/java/com/android/server/accessibility/TouchExplorer.java @@ -16,9 +16,6 @@ package com.android.server.accessibility; -import static android.view.accessibility.AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_END; -import static android.view.accessibility.AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_START; - import android.content.Context; import android.gesture.Gesture; import android.gesture.GestureLibraries; @@ -26,17 +23,19 @@ import android.gesture.GestureLibrary; import android.gesture.GesturePoint; import android.gesture.GestureStroke; import android.gesture.Prediction; +import android.graphics.Rect; import android.os.Handler; +import android.os.SystemClock; import android.util.Slog; import android.view.MotionEvent; +import android.view.MotionEvent.PointerCoords; +import android.view.MotionEvent.PointerProperties; import android.view.VelocityTracker; import android.view.ViewConfiguration; import android.view.WindowManagerPolicy; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityManager; -import com.android.server.input.InputFilter; import com.android.internal.R; +import com.android.server.input.InputFilter; import java.util.ArrayList; import java.util.Arrays; @@ -47,17 +46,18 @@ import java.util.Arrays; * and consuming certain events. The interaction model is: * * <ol> - * <li>1. One finger moving around performs touch exploration.</li> - * <li>2. Two close fingers moving in the same direction perform a drag.</li> - * <li>3. Multi-finger gestures are delivered to view hierarchy.</li> - * <li>4. Pointers that have not moved more than a specified distance after they + * <li>1. One finger moving slow around performs touch exploration.</li> + * <li>2. One finger moving fast around performs gestures.</li> + * <li>3. Two close fingers moving in the same direction perform a drag.</li> + * <li>4. Multi-finger gestures are delivered to view hierarchy.</li> + * <li>5. Pointers that have not moved more than a specified distance after they * went down are considered inactive.</li> - * <li>5. Two fingers moving too far from each other or in different directions - * are considered a multi-finger gesture.</li> - * <li>6. Tapping on the last touch explored location within given time and - * distance slop performs a click.</li> - * <li>7. Tapping and holding for a while on the last touch explored location within - * given time and distance slop performs a long press.</li> + * <li>6. Two fingers moving in different directions are considered a multi-finger gesture.</li> + * <li>7. Double tapping clicks on the on the last touch explored location of it was in + * a window that does not take focus, otherwise the click is within the accessibility + * focused rectangle.</li> + * <li>7. Tapping and holding for a while performs a long press in a similar fashion + * as the click above.</li> * <ol> * * @hide @@ -75,85 +75,116 @@ public class TouchExplorer { private static final int STATE_DELEGATING = 0x00000004; private static final int STATE_GESTURE_DETECTING = 0x00000005; - // The time slop in milliseconds for activating an item after it has - // been touch explored. Tapping on an item within this slop will perform - // a click and tapping and holding down a long press. - private static final long ACTIVATION_TIME_SLOP = 2000; - // The minimum of the cosine between the vectors of two moving // pointers so they can be considered moving in the same direction. private static final float MAX_DRAGGING_ANGLE_COS = 0.525321989f; // cos(pi/4) - // The delay for sending a hover enter event. - private static final long DELAY_SEND_HOVER_ENTER = 200; - // Constant referring to the ids bits of all pointers. private static final int ALL_POINTER_ID_BITS = 0xFFFFFFFF; // This constant captures the current implementation detail that // pointer IDs are between 0 and 31 inclusive (subject to change). // (See MAX_POINTER_ID in frameworks/base/include/ui/Input.h) - public static final int MAX_POINTER_COUNT = 32; + private static final int MAX_POINTER_COUNT = 32; // Invalid pointer ID. - public static final int INVALID_POINTER_ID = -1; + private static final int INVALID_POINTER_ID = -1; + + // The velocity above which we detect gestures. + private static final int GESTURE_DETECTION_VELOCITY_DIP = 1000; + + // The minimal distance before we take the middle of the distance between + // the two dragging pointers as opposed to use the location of the primary one. + private static final int MIN_POINTER_DISTANCE_TO_USE_MIDDLE_LOCATION_DIP = 200; // Temporary array for storing pointer IDs. private final int[] mTempPointerIds = new int[MAX_POINTER_COUNT]; - // The distance from the last touch explored location tapping within - // which would perform a click and tapping and holding a long press. - private final int mTouchExplorationTapSlop; + // Timeout within which we try to detect a tap. + private final int mTapTimeout; + + // Timeout within which we try to detect a double tap. + private final int mDoubleTapTimeout; + + // Slop between the down and up tap to be a tap. + private final int mTouchSlop; + + // Slop between the first and second tap to be a double tap. + private final int mDoubleTapSlop; // The InputFilter this tracker is associated with i.e. the filter // which delegates event processing to this touch explorer. private final InputFilter mInputFilter; - // Handle to the accessibility manager for firing accessibility events - // announcing touch exploration gesture start and end. - private final AccessibilityManager mAccessibilityManager; - - // The last event that was received while performing touch exploration. - private MotionEvent mLastTouchExploreEvent; - // The current state of the touch explorer. private int mCurrentState = STATE_TOUCH_EXPLORING; - // Flag whether a touch exploration gesture is in progress. - private boolean mTouchExploreGestureInProgress; - // The ID of the pointer used for dragging. private int mDraggingPointerId; // Handler for performing asynchronous operations. private final Handler mHandler; - // Command for delayed sending of a hover event. - private final SendHoverDelayed mSendHoverDelayed; + // Command for delayed sending of a hover enter event. + private final SendHoverDelayed mSendHoverEnterDelayed; + + // Command for delayed sending of a hover exit event. + private final SendHoverDelayed mSendHoverExitDelayed; // Command for delayed sending of a long press. private final PerformLongPressDelayed mPerformLongPressDelayed; + // Helper to detect and react to double tap in touch explore mode. + private final DoubleTapDetector mDoubleTapDetector; + + // The scaled minimal distance before we take the middle of the distance between + // the two dragging pointers as opposed to use the location of the primary one. + private final int mScaledMinPointerDistanceToUseMiddleLocation; + + // The scaled velocity above which we detect gestures. + private final int mScaledGestureDetectionVelocity; + + // Helper to track gesture velocity. private VelocityTracker mVelocityTracker; + // Helper class to track received pointers. private final ReceivedPointerTracker mReceivedPointerTracker; + // Helper class to track injected pointers. private final InjectedPointerTracker mInjectedPointerTracker; - private final GestureListener mGestureListener; + // Handle to the accessibility manager service. + private final AccessibilityManagerService mAms; - /** - * Callback for gesture detection. - */ - public interface GestureListener { + // Temporary rectangle to avoid instantiation. + private final Rect mTempRect = new Rect(); - /** - * Called when a given gesture was performed. - * - * @param gestureId The gesture id. - */ - public boolean onGesture(int gestureId); - } + // The X of the previous event. + private float mPreviousX; + + // The Y of the previous event. + private float mPreviousY; + + // Buffer for storing points for gesture detection. + private final ArrayList<GesturePoint> mStrokeBuffer = new ArrayList<GesturePoint>(100); + + // The minimal delta between moves to add a gesture point. + private static final int TOUCH_TOLERANCE = 3; + + // The minimal score for accepting a predicted gesture. + private static final float MIN_PREDICTION_SCORE = 2.0f; + + // The library for gesture detection. + private GestureLibrary mGestureLibrary; + + // The long pressing pointer id if coordinate remapping is needed. + private int mLongPressingPointerId; + + // The long pressing pointer X if coordinate remapping is needed. + private int mLongPressingPointerDeltaX; + + // The long pressing pointer Y if coordinate remapping is needed. + private int mLongPressingPointerDeltaY; /** * Creates a new instance. @@ -162,25 +193,73 @@ public class TouchExplorer { * @param context A context handle for accessing resources. */ public TouchExplorer(InputFilter inputFilter, Context context, - GestureListener gestureListener) { - mGestureListener = gestureListener; + AccessibilityManagerService service) { + mAms = service; mReceivedPointerTracker = new ReceivedPointerTracker(context); mInjectedPointerTracker = new InjectedPointerTracker(); mInputFilter = inputFilter; - mTouchExplorationTapSlop = - ViewConfiguration.get(context).getScaledTouchExploreTapSlop(); + mTapTimeout = ViewConfiguration.getTapTimeout(); + mDoubleTapTimeout = ViewConfiguration.getDoubleTapTimeout(); + mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); + mDoubleTapSlop = ViewConfiguration.get(context).getScaledDoubleTapSlop(); mHandler = new Handler(context.getMainLooper()); - mSendHoverDelayed = new SendHoverDelayed(); mPerformLongPressDelayed = new PerformLongPressDelayed(); - mAccessibilityManager = AccessibilityManager.getInstance(context); mGestureLibrary = GestureLibraries.fromRawResource(context, R.raw.accessibility_gestures); mGestureLibrary.setOrientationStyle(4); mGestureLibrary.load(); + mSendHoverEnterDelayed = new SendHoverDelayed(MotionEvent.ACTION_HOVER_ENTER, true); + mSendHoverExitDelayed = new SendHoverDelayed(MotionEvent.ACTION_HOVER_EXIT, false); + mDoubleTapDetector = new DoubleTapDetector(); + final float density = context.getResources().getDisplayMetrics().density; + mScaledMinPointerDistanceToUseMiddleLocation = + (int) (MIN_POINTER_DISTANCE_TO_USE_MIDDLE_LOCATION_DIP * density); + mScaledGestureDetectionVelocity = (int) (GESTURE_DETECTION_VELOCITY_DIP * density); + } + + public void clear() { + // If we have not received an event then we are in initial + // state. Therefore, there is not need to clean anything. + MotionEvent event = mReceivedPointerTracker.getLastReceivedEvent(); + if (event != null) { + clear(mReceivedPointerTracker.getLastReceivedEvent(), WindowManagerPolicy.FLAG_TRUSTED); + } } public void clear(MotionEvent event, int policyFlags) { - sendUpForInjectedDownPointers(event, policyFlags); - clear(); + switch (mCurrentState) { + case STATE_TOUCH_EXPLORING: { + // If a touch exploration gesture is in progress send events for its end. + sendExitEventsIfNeeded(policyFlags); + } break; + case STATE_DRAGGING: { + mDraggingPointerId = INVALID_POINTER_ID; + // Send exit to all pointers that we have delivered. + sendUpForInjectedDownPointers(event, policyFlags); + } break; + case STATE_DELEGATING: { + // Send exit to all pointers that we have delivered. + sendUpForInjectedDownPointers(event, policyFlags); + } break; + case STATE_GESTURE_DETECTING: { + // Clear the current stroke. + mStrokeBuffer.clear(); + } break; + } + // Remove all pending callbacks. + mSendHoverEnterDelayed.remove(); + mSendHoverExitDelayed.remove(); + mPerformLongPressDelayed.remove(); + // Reset the pointer trackers. + mReceivedPointerTracker.clear(); + mInjectedPointerTracker.clear(); + // Clear the double tap detector + mDoubleTapDetector.clear(); + // Go to initial state. + // Clear the long pressing pointer remap data. + mLongPressingPointerId = -1; + mLongPressingPointerDeltaX = 0; + mLongPressingPointerDeltaY = 0; + mCurrentState = STATE_TOUCH_EXPLORING; } public void onMotionEvent(MotionEvent event, int policyFlags) { @@ -218,7 +297,6 @@ public class TouchExplorer { */ private void handleMotionEventStateTouchExploring(MotionEvent event, int policyFlags) { ReceivedPointerTracker receivedTracker = mReceivedPointerTracker; - InjectedPointerTracker injectedTracker = mInjectedPointerTracker; final int activePointerCount = receivedTracker.getActivePointerCount(); if (mVelocityTracker == null) { @@ -226,8 +304,16 @@ public class TouchExplorer { } mVelocityTracker.addMovement(event); + mDoubleTapDetector.onMotionEvent(event, policyFlags); + switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: + // Pre-feed the motion events to the gesture detector since we + // have a distance slop before getting into gesture detection + // mode and not using the points within this slop significantly + // decreases the quality of gesture recognition. + handleMotionEventGestureDetecting(event, policyFlags); + //$FALL-THROUGH$ case MotionEvent.ACTION_POINTER_DOWN: { switch (activePointerCount) { case 0: { @@ -235,44 +321,31 @@ public class TouchExplorer { + "touch exploring state!"); } case 1: { - mSendHoverDelayed.remove(); - mPerformLongPressDelayed.remove(); - // Send a hover for every finger down so the user gets feedback. - final int pointerId = receivedTracker.getPrimaryActivePointerId(); - final int pointerIdBits = (1 << pointerId); - final int lastAction = injectedTracker.getLastInjectedHoverAction(); - - // Deliver hover enter with a delay to have a change to detect - // whether the user actually starts a scrolling gesture. - if (lastAction == MotionEvent.ACTION_HOVER_EXIT) { - mSendHoverDelayed.post(event, MotionEvent.ACTION_HOVER_ENTER, - pointerIdBits, policyFlags, DELAY_SEND_HOVER_ENTER); - } else { - sendMotionEvent(event, MotionEvent.ACTION_HOVER_MOVE, pointerIdBits, - policyFlags); - } - - if (mLastTouchExploreEvent == null) { - break; + // If we still have not notified the user for the last + // touch, we figure out what to do. If were waiting + // we resent the delayed callback and wait again. + if (mSendHoverEnterDelayed.isPending()) { + mSendHoverEnterDelayed.remove(); + mSendHoverExitDelayed.remove(); + mPerformLongPressDelayed.remove(); } - // If more pointers down on the screen since the last touch - // exploration we discard the last cached touch explore event. - if (event.getPointerCount() != mLastTouchExploreEvent.getPointerCount()) { - mLastTouchExploreEvent = null; - break; - } - - // If the down is in the time slop => schedule a long press. - final long pointerDownTime = - receivedTracker.getReceivedPointerDownTime(pointerId); - final long lastExploreTime = mLastTouchExploreEvent.getEventTime(); - final long deltaTimeExplore = pointerDownTime - lastExploreTime; - if (deltaTimeExplore <= ACTIVATION_TIME_SLOP) { - mPerformLongPressDelayed.post(event, policyFlags, - ViewConfiguration.getLongPressTimeout()); + // If we have the first tap schedule a long press and break + // since we do not want to schedule hover enter because + // the delayed callback will kick in before the long click. + // This would lead to a state transition resulting in long + // pressing the item below the double taped area which is + // not necessary where accessibility focus is. + if (mDoubleTapDetector.firstTapDetected()) { + // We got a tap now post a long press action. + mPerformLongPressDelayed.post(event, policyFlags); break; } + // Deliver hover enter with a delay to have a chance + // to detect what the user is trying to do. + final int pointerId = receivedTracker.getPrimaryActivePointerId(); + final int pointerIdBits = (1 << pointerId); + mSendHoverEnterDelayed.post(event, pointerIdBits, policyFlags); } break; default: { /* do nothing - let the code for ACTION_MOVE decide what to do */ @@ -288,119 +361,130 @@ public class TouchExplorer { /* do nothing - no active pointers so we swallow the event */ } break; case 1: { - // Detect touch exploration gesture start by having one active pointer - // that moved more than a given distance. - if (!mTouchExploreGestureInProgress) { + // We have not started sending events since we try to + // figure out what the user is doing. + if (mSendHoverEnterDelayed.isPending()) { + // Pre-feed the motion events to the gesture detector since we + // have a distance slop before getting into gesture detection + // mode and not using the points within this slop significantly + // decreases the quality of gesture recognition. + handleMotionEventGestureDetecting(event, policyFlags); + final float deltaX = receivedTracker.getReceivedPointerDownX(pointerId) - event.getX(pointerIndex); final float deltaY = receivedTracker.getReceivedPointerDownY(pointerId) - event.getY(pointerIndex); final double moveDelta = Math.hypot(deltaX, deltaY); - - if (moveDelta > mTouchExplorationTapSlop) { - + // The user has moved enough for us to decide. + if (moveDelta > mDoubleTapSlop) { + // Check whether the user is performing a gesture. We + // detect gestures if the pointer is moving above a + // given velocity. mVelocityTracker.computeCurrentVelocity(1000); final float maxAbsVelocity = Math.max( Math.abs(mVelocityTracker.getXVelocity(pointerId)), Math.abs(mVelocityTracker.getYVelocity(pointerId))); - // TODO: Tune the velocity cut off and add a constant. - if (maxAbsVelocity > 1000) { - clear(event, policyFlags); + if (maxAbsVelocity > mScaledGestureDetectionVelocity) { + // We have to perform gesture detection, so + // clear the current state and try to detect. mCurrentState = STATE_GESTURE_DETECTING; - event.setAction(MotionEvent.ACTION_DOWN); - handleMotionEventGestureDetecting(event, policyFlags); - return; - } - - mTouchExploreGestureInProgress = true; - sendAccessibilityEvent(TYPE_TOUCH_EXPLORATION_GESTURE_START); - // Make sure the scheduled down/move event is sent. - mSendHoverDelayed.forceSendAndRemove(); - mPerformLongPressDelayed.remove(); - // If we have transitioned to exploring state from another one - // we need to send a hover enter event here. - final int lastAction = injectedTracker.getLastInjectedHoverAction(); - if (lastAction == MotionEvent.ACTION_HOVER_EXIT) { - sendMotionEvent(event, MotionEvent.ACTION_HOVER_ENTER, + mSendHoverEnterDelayed.remove(); + mSendHoverExitDelayed.remove(); + mPerformLongPressDelayed.remove(); + } else { + // We have just decided that the user is touch, + // exploring so start sending events. + mSendHoverEnterDelayed.forceSendAndRemove(); + mSendHoverExitDelayed.remove(); + sendMotionEvent(event, MotionEvent.ACTION_HOVER_MOVE, pointerIdBits, policyFlags); } - sendMotionEvent(event, MotionEvent.ACTION_HOVER_MOVE, pointerIdBits, - policyFlags); + break; } } else { - // Touch exploration gesture in progress so send a hover event. + // The user is wither double tapping or performing long + // press so do not send move events yet. + if (mDoubleTapDetector.firstTapDetected()) { + break; + } + sendEnterEventsIfNeeded(policyFlags); sendMotionEvent(event, MotionEvent.ACTION_HOVER_MOVE, pointerIdBits, policyFlags); } - - // If the exploring pointer moved enough => cancel the long press. - if (!mTouchExploreGestureInProgress && mLastTouchExploreEvent != null - && mPerformLongPressDelayed.isPenidng()) { - - // If the pointer moved more than the tap slop => cancel long press. - final float deltaX = mLastTouchExploreEvent.getX(pointerIndex) + } break; + case 2: { + // More than one pointer so the user is not touch exploring + // and now we have to decide whether to delegate or drag. + if (mSendHoverEnterDelayed.isPending()) { + // We have not started sending events so cancel + // scheduled sending events. + mSendHoverEnterDelayed.remove(); + mSendHoverExitDelayed.remove(); + mPerformLongPressDelayed.remove(); + } else { + // If the user is touch exploring the second pointer may be + // performing a double tap to activate an item without need + // for the user to lift his exploring finger. + final float deltaX = receivedTracker.getReceivedPointerDownX(pointerId) - event.getX(pointerIndex); - final float deltaY = mLastTouchExploreEvent.getY(pointerIndex) + final float deltaY = receivedTracker.getReceivedPointerDownY(pointerId) - event.getY(pointerIndex); - final float moveDelta = (float) Math.hypot(deltaX, deltaY); - if (moveDelta > mTouchExplorationTapSlop) { - mLastTouchExploreEvent = null; - mPerformLongPressDelayed.remove(); + final double moveDelta = Math.hypot(deltaX, deltaY); + if (moveDelta < mDoubleTapSlop) { break; } + // We are sending events so send exit and gesture + // end since we transition to another state. + sendExitEventsIfNeeded(policyFlags); } - } break; - case 2: { - mSendHoverDelayed.remove(); - mPerformLongPressDelayed.remove(); - // We want to no longer hover over the location so subsequent - // touch at the same spot will generate a hover enter. - ensureHoverExitSent(event, pointerIdBits, policyFlags); + + // We know that a new state transition is to happen and the + // new state will not be gesture recognition, so clear the + // stashed gesture strokes. + mStrokeBuffer.clear(); if (isDraggingGesture(event)) { // Two pointers moving in the same direction within // a given distance perform a drag. + mSendHoverEnterDelayed.remove(); + mSendHoverExitDelayed.remove(); + mPerformLongPressDelayed.remove(); mCurrentState = STATE_DRAGGING; - if (mTouchExploreGestureInProgress) { - sendAccessibilityEvent(TYPE_TOUCH_EXPLORATION_GESTURE_END); - mTouchExploreGestureInProgress = false; - } - mLastTouchExploreEvent = null; mDraggingPointerId = pointerId; sendMotionEvent(event, MotionEvent.ACTION_DOWN, pointerIdBits, policyFlags); } else { // Two pointers moving arbitrary are delegated to the view hierarchy. mCurrentState = STATE_DELEGATING; - mSendHoverDelayed.remove(); - if (mTouchExploreGestureInProgress) { - sendAccessibilityEvent(TYPE_TOUCH_EXPLORATION_GESTURE_END); - mTouchExploreGestureInProgress = false; - } - mLastTouchExploreEvent = null; sendDownForAllActiveNotInjectedPointers(event, policyFlags); } } break; default: { - mSendHoverDelayed.remove(); - mPerformLongPressDelayed.remove(); - // We want to no longer hover over the location so subsequent - // touch at the same spot will generate a hover enter. - ensureHoverExitSent(event, pointerIdBits, policyFlags); + // More than one pointer so the user is not touch exploring + // and now we have to decide whether to delegate or drag. + if (mSendHoverEnterDelayed.isPending()) { + // We have not started sending events so cancel + // scheduled sending events. + mSendHoverEnterDelayed.remove(); + mSendHoverExitDelayed.remove(); + mPerformLongPressDelayed.remove(); + } else { + // We are sending events so send exit and gesture + // end since we transition to another state. + sendExitEventsIfNeeded(policyFlags); + } // More than two pointers are delegated to the view hierarchy. mCurrentState = STATE_DELEGATING; - mSendHoverDelayed.remove(); - if (mTouchExploreGestureInProgress) { - sendAccessibilityEvent(TYPE_TOUCH_EXPLORATION_GESTURE_END); - mTouchExploreGestureInProgress = false; - } - mLastTouchExploreEvent = null; sendDownForAllActiveNotInjectedPointers(event, policyFlags); } } } break; case MotionEvent.ACTION_UP: + // We know that we do not need the pre-fed gesture points are not + // needed anymore since the last pointer just went up. + mStrokeBuffer.clear(); + //$FALL-THROUGH$ case MotionEvent.ACTION_POINTER_UP: { final int pointerId = receivedTracker.getLastReceivedUpPointerId(); final int pointerIdBits = (1 << pointerId); @@ -413,59 +497,12 @@ public class TouchExplorer { mPerformLongPressDelayed.remove(); - // If touch exploring announce the end of the gesture. - // Also do not click on the last explored location. - if (mTouchExploreGestureInProgress) { - mTouchExploreGestureInProgress = false; - mSendHoverDelayed.forceSendAndRemove(); - ensureHoverExitSent(event, pointerIdBits, policyFlags); - mLastTouchExploreEvent = MotionEvent.obtain(event); - sendAccessibilityEvent(TYPE_TOUCH_EXPLORATION_GESTURE_END); - break; - } - - // Detect whether to activate i.e. click on the last explored location. - if (mLastTouchExploreEvent != null) { - // If the down was not in the time slop => nothing else to do. - final long eventTime = - receivedTracker.getLastReceivedUpPointerDownTime(); - final long exploreTime = mLastTouchExploreEvent.getEventTime(); - final long deltaTime = eventTime - exploreTime; - if (deltaTime > ACTIVATION_TIME_SLOP) { - mSendHoverDelayed.forceSendAndRemove(); - ensureHoverExitSent(event, pointerIdBits, policyFlags); - mLastTouchExploreEvent = MotionEvent.obtain(event); - break; - } - - // If a tap is farther than the tap slop => nothing to do. - final int pointerIndex = event.findPointerIndex(pointerId); - final float deltaX = mLastTouchExploreEvent.getX(pointerIndex) - - event.getX(pointerIndex); - final float deltaY = mLastTouchExploreEvent.getY(pointerIndex) - - event.getY(pointerIndex); - final float deltaMove = (float) Math.hypot(deltaX, deltaY); - if (deltaMove > mTouchExplorationTapSlop) { - mSendHoverDelayed.forceSendAndRemove(); - ensureHoverExitSent(event, pointerIdBits, policyFlags); - mLastTouchExploreEvent = MotionEvent.obtain(event); - break; - } - - // This is a tap so do not send hover events since - // this events will result in firing the corresponding - // accessibility events confusing the user about what - // is actually clicked. - mSendHoverDelayed.remove(); - ensureHoverExitSent(event, pointerIdBits, policyFlags); - - // All preconditions are met, so click the last explored location. - sendActionDownAndUp(mLastTouchExploreEvent, policyFlags); - mLastTouchExploreEvent = null; + // If we have not delivered the enter schedule exit. + if (mSendHoverEnterDelayed.isPending()) { + mSendHoverExitDelayed.post(event, pointerIdBits, policyFlags); } else { - mSendHoverDelayed.forceSendAndRemove(); - ensureHoverExitSent(event, pointerIdBits, policyFlags); - mLastTouchExploreEvent = MotionEvent.obtain(event); + // The user is touch exploring so we send events for end. + sendExitEventsIfNeeded(policyFlags); } } break; } @@ -475,16 +512,7 @@ public class TouchExplorer { } } break; case MotionEvent.ACTION_CANCEL: { - mSendHoverDelayed.remove(); - mPerformLongPressDelayed.remove(); - final int pointerId = receivedTracker.getPrimaryActivePointerId(); - final int pointerIdBits = (1 << pointerId); - ensureHoverExitSent(event, pointerIdBits, policyFlags); - clear(); - if (mVelocityTracker != null) { - mVelocityTracker.clear(); - mVelocityTracker = null; - } + clear(event, policyFlags); } break; } } @@ -517,6 +545,28 @@ public class TouchExplorer { } break; case 2: { if (isDraggingGesture(event)) { + // If the dragging pointer are closer that a given distance we + // use the location of the primary one. Otherwise, we take the + // middle between the pointers. + int[] pointerIds = mTempPointerIds; + mReceivedPointerTracker.populateActivePointerIds(pointerIds); + + final int firstPtrIndex = event.findPointerIndex(pointerIds[0]); + final int secondPtrIndex = event.findPointerIndex(pointerIds[1]); + + final float firstPtrX = event.getX(firstPtrIndex); + final float firstPtrY = event.getY(firstPtrIndex); + final float secondPtrX = event.getX(secondPtrIndex); + final float secondPtrY = event.getY(secondPtrIndex); + + final float deltaX = firstPtrX - secondPtrX; + final float deltaY = firstPtrY - secondPtrY; + final double distance = Math.hypot(deltaX, deltaY); + + if (distance > mScaledMinPointerDistanceToUseMiddleLocation) { + event.setLocation(deltaX / 2, deltaY / 2); + } + // If still dragging send a drag event. sendMotionEvent(event, MotionEvent.ACTION_MOVE, pointerIdBits, policyFlags); @@ -557,7 +607,7 @@ public class TouchExplorer { mCurrentState = STATE_TOUCH_EXPLORING; } break; case MotionEvent.ACTION_CANCEL: { - clear(); + clear(event, policyFlags); } break; } } @@ -574,9 +624,6 @@ public class TouchExplorer { throw new IllegalStateException("Delegating state can only be reached if " + "there is at least one pointer down!"); } - case MotionEvent.ACTION_UP: { - mCurrentState = STATE_TOUCH_EXPLORING; - } break; case MotionEvent.ACTION_MOVE: { // Check whether some other pointer became active because they have moved // a given distance and if such exist send them to the view hierarchy @@ -587,30 +634,24 @@ public class TouchExplorer { sendDownForAllActiveNotInjectedPointers(prototype, policyFlags); } } break; + case MotionEvent.ACTION_UP: case MotionEvent.ACTION_POINTER_UP: { + mLongPressingPointerId = -1; + mLongPressingPointerDeltaX = 0; + mLongPressingPointerDeltaY = 0; // No active pointers => go to initial state. if (mReceivedPointerTracker.getActivePointerCount() == 0) { mCurrentState = STATE_TOUCH_EXPLORING; } } break; case MotionEvent.ACTION_CANCEL: { - clear(); + clear(event, policyFlags); } break; } // Deliver the event striping out inactive pointers. sendMotionEventStripInactivePointers(event, policyFlags); } - private float mPreviousX; - private float mPreviousY; - - private final ArrayList<GesturePoint> mStrokeBuffer = new ArrayList<GesturePoint>(100); - - private static final int TOUCH_TOLERANCE = 3; - private static final float MIN_PREDICTION_SCORE = 2.0f; - - private GestureLibrary mGestureLibrary; - private void handleMotionEventGestureDetecting(MotionEvent event, int policyFlags) { switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: { @@ -631,8 +672,7 @@ public class TouchExplorer { mStrokeBuffer.add(new GesturePoint(x, y, event.getEventTime())); } } break; - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_POINTER_UP: { + case MotionEvent.ACTION_UP: { float x = event.getX(); float y = event.getY(); mStrokeBuffer.add(new GesturePoint(x, y, event.getEventTime())); @@ -650,7 +690,7 @@ public class TouchExplorer { } try { final int gestureId = Integer.parseInt(bestPrediction.name); - mGestureListener.onGesture(gestureId); + mAms.onGesture(gestureId); } catch (NumberFormatException nfe) { Slog.w(LOG_TAG, "Non numeric gesture id:" + bestPrediction.name); } @@ -661,8 +701,7 @@ public class TouchExplorer { mCurrentState = STATE_TOUCH_EXPLORING; } break; case MotionEvent.ACTION_CANCEL: { - mStrokeBuffer.clear(); - mCurrentState = STATE_TOUCH_EXPLORING; + clear(event, policyFlags); } break; } } @@ -706,17 +745,32 @@ public class TouchExplorer { } /** - * Ensures that hover exit has been sent. + * Sends the exit events if needed. Such events are hover exit and touch explore + * gesture end. * - * @param prototype The prototype from which to create the injected events. - * @param pointerIdBits The bits of the pointers to send. * @param policyFlags The policy flags associated with the event. */ - private void ensureHoverExitSent(MotionEvent prototype, int pointerIdBits, int policyFlags) { - final int lastAction = mInjectedPointerTracker.getLastInjectedHoverAction(); - if (lastAction != MotionEvent.ACTION_HOVER_EXIT) { - sendMotionEvent(prototype, MotionEvent.ACTION_HOVER_EXIT, pointerIdBits, - policyFlags); + private void sendExitEventsIfNeeded(int policyFlags) { + MotionEvent event = mInjectedPointerTracker.getLastInjectedHoverEvent(); + if (event != null && event.getActionMasked() != MotionEvent.ACTION_HOVER_EXIT) { + final int pointerIdBits = event.getPointerIdBits(); + mAms.touchExplorationGestureEnded(); + sendMotionEvent(event, MotionEvent.ACTION_HOVER_EXIT, pointerIdBits, policyFlags); + } + } + + /** + * Sends the enter events if needed. Such events are hover enter and touch explore + * gesture start. + * + * @param policyFlags The policy flags associated with the event. + */ + private void sendEnterEventsIfNeeded(int policyFlags) { + MotionEvent event = mInjectedPointerTracker.getLastInjectedHoverEvent(); + if (event != null && event.getActionMasked() == MotionEvent.ACTION_HOVER_EXIT) { + final int pointerIdBits = event.getPointerIdBits(); + mAms.touchExplorationGestureStarted(); + sendMotionEvent(event, MotionEvent.ACTION_HOVER_ENTER, pointerIdBits, policyFlags); } } @@ -826,6 +880,36 @@ public class TouchExplorer { event.setDownTime(mInjectedPointerTracker.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) { + 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 -= mLongPressingPointerDeltaX; + coords[i].y -= mLongPressingPointerDeltaY; + } + } + MotionEvent remapped = 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.getFlags()); + if (event != prototype) { + event.recycle(); + } + event = remapped; + } + if (DEBUG) { Slog.d(LOG_TAG, "Injecting event: " + event + ", policyFlags=0x" + Integer.toHexString(policyFlags)); @@ -878,6 +962,172 @@ public class TouchExplorer { } } + private class DoubleTapDetector { + private MotionEvent mDownEvent; + private MotionEvent mFirstTapEvent; + + public void onMotionEvent(MotionEvent event, int policyFlags) { + final int action = event.getActionMasked(); + switch (action) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_POINTER_DOWN: { + if (mFirstTapEvent != null && !isSamePointerContext(mFirstTapEvent, event)) { + clear(); + } + mDownEvent = MotionEvent.obtain(event); + } break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_POINTER_UP: { + if (mDownEvent == null) { + return; + } + if (!isSamePointerContext(mDownEvent, event)) { + clear(); + return; + } + if (isTap(mDownEvent, event)) { + if (mFirstTapEvent == null || isTimedOut(mFirstTapEvent, event, + mDoubleTapTimeout)) { + mFirstTapEvent = MotionEvent.obtain(event); + mDownEvent.recycle(); + mDownEvent = null; + return; + } + if (isDoubleTap(mFirstTapEvent, event)) { + onDoubleTap(event, policyFlags); + mFirstTapEvent.recycle(); + mFirstTapEvent = null; + mDownEvent.recycle(); + mDownEvent = null; + return; + } + mFirstTapEvent.recycle(); + mFirstTapEvent = null; + } else { + if (mFirstTapEvent != null) { + mFirstTapEvent.recycle(); + mFirstTapEvent = null; + } + } + mDownEvent.recycle(); + mDownEvent = null; + } break; + } + } + + public void onDoubleTap(MotionEvent secondTapUp, int policyFlags) { + // This should never be called when more than two pointers are down. + if (secondTapUp.getPointerCount() > 2) { + return; + } + + // Remove pending event deliveries. + mSendHoverEnterDelayed.remove(); + mSendHoverExitDelayed.remove(); + mPerformLongPressDelayed.remove(); + + // This is a tap so do not send hover events since + // this events will result in firing the corresponding + // accessibility events confusing the user about what + // is actually clicked. + sendExitEventsIfNeeded(policyFlags); + + // If the last touched explored location is not within the focused + // window we will click at that exact spot, otherwise we find the + // accessibility focus and if the tap is within its bounds we click + // there, otherwise we pick the middle of the focus rectangle. + MotionEvent lastEvent = mInjectedPointerTracker.getLastInjectedHoverEvent(); + if (lastEvent == null) { + return; + } + + final int exploreLocationX = (int) lastEvent.getX(lastEvent.getActionIndex()); + final int exploreLocationY = (int) lastEvent.getY(lastEvent.getActionIndex()); + + Rect bounds = mTempRect; + boolean useLastHoverLocation = false; + + final int pointerId = secondTapUp.getPointerId(secondTapUp.getActionIndex()); + final int pointerIndex = secondTapUp.findPointerIndex(pointerId); + if (mAms.getAccessibilityFocusBounds(exploreLocationX, exploreLocationY, bounds)) { + // If the user's last touch explored location is not + // within the accessibility focus bounds we use the center + // of the accessibility focused rectangle. + if (!bounds.contains((int) secondTapUp.getX(pointerIndex), + (int) secondTapUp.getY(pointerIndex))) { + useLastHoverLocation = true; + } + } + + // Do the click. + PointerProperties[] properties = new PointerProperties[1]; + properties[0] = new PointerProperties(); + secondTapUp.getPointerProperties(pointerIndex, properties[0]); + PointerCoords[] coords = new PointerCoords[1]; + coords[0] = new PointerCoords(); + coords[0].x = (useLastHoverLocation) ? bounds.centerX() : exploreLocationX; + coords[0].y = (useLastHoverLocation) ? bounds.centerY() : exploreLocationY; + MotionEvent event = MotionEvent.obtain(secondTapUp.getDownTime(), + secondTapUp.getEventTime(), MotionEvent.ACTION_DOWN, 1, properties, + coords, 0, 0, 1.0f, 1.0f, secondTapUp.getDeviceId(), 0, + secondTapUp.getSource(), secondTapUp.getFlags()); + sendActionDownAndUp(event, policyFlags); + event.recycle(); + } + + public void clear() { + if (mDownEvent != null) { + mDownEvent.recycle(); + mDownEvent = null; + } + if (mFirstTapEvent != null) { + mFirstTapEvent.recycle(); + mFirstTapEvent = null; + } + } + + public boolean isTap(MotionEvent down, MotionEvent up) { + return eventsWithinTimeoutAndDistance(down, up, mTapTimeout, mTouchSlop); + } + + private boolean isDoubleTap(MotionEvent firstUp, MotionEvent secondUp) { + return eventsWithinTimeoutAndDistance(firstUp, secondUp, mDoubleTapTimeout, + mDoubleTapSlop); + } + + private boolean eventsWithinTimeoutAndDistance(MotionEvent first, MotionEvent second, + int timeout, int distance) { + if (isTimedOut(first, second, timeout)) { + return false; + } + final int downPtrIndex = first.getActionIndex(); + final int upPtrIndex = second.getActionIndex(); + final float deltaX = second.getX(upPtrIndex) - first.getX(downPtrIndex); + final float deltaY = second.getY(upPtrIndex) - first.getY(downPtrIndex); + final double deltaMove = Math.hypot(deltaX, deltaY); + if (deltaMove >= distance) { + return false; + } + return true; + } + + private boolean isTimedOut(MotionEvent firstUp, MotionEvent secondUp, int timeout) { + final long deltaTime = secondUp.getEventTime() - firstUp.getEventTime(); + return (deltaTime >= timeout); + } + + private boolean isSamePointerContext(MotionEvent first, MotionEvent second) { + return (first.getPointerIdBits() == second.getPointerIdBits() + && first.getPointerId(first.getActionIndex()) + == second.getPointerId(second.getActionIndex())); + } + + public boolean firstTapDetected() { + return mFirstTapEvent != null + && SystemClock.uptimeMillis() - mFirstTapEvent.getEventTime() < mDoubleTapTimeout; + } + } + /** * Determines whether a two pointer gesture is a dragging one. * @@ -940,30 +1190,6 @@ public class TouchExplorer { return true; } - /** - * Sends an event announcing the start/end of a touch exploration gesture. - * - * @param eventType The type of the event to send. - */ - private void sendAccessibilityEvent(int eventType) { - AccessibilityEvent event = AccessibilityEvent.obtain(eventType); - mAccessibilityManager.sendAccessibilityEvent(event); - } - - /** - * Clears the internal state of this explorer. - */ - public void clear() { - mSendHoverDelayed.remove(); - mPerformLongPressDelayed.remove(); - mReceivedPointerTracker.clear(); - mInjectedPointerTracker.clear(); - mLastTouchExploreEvent = null; - mCurrentState = STATE_TOUCH_EXPLORING; - mTouchExploreGestureInProgress = false; - mDraggingPointerId = INVALID_POINTER_ID; - } - /** * Gets the symbolic name of a state. * @@ -1002,10 +1228,10 @@ public class TouchExplorer { private MotionEvent mEvent; private int mPolicyFlags; - public void post(MotionEvent prototype, int policyFlags, long delay) { + public void post(MotionEvent prototype, int policyFlags) { mEvent = MotionEvent.obtain(prototype); mPolicyFlags = policyFlags; - mHandler.postDelayed(this, delay); + mHandler.postDelayed(this, ViewConfiguration.getLongPressTimeout()); } public void remove() { @@ -1021,16 +1247,29 @@ public class TouchExplorer { @Override public void run() { - mCurrentState = STATE_DELEGATING; - // Make sure the scheduled hover exit is delivered. - mSendHoverDelayed.remove(); + final int pointerIndex = mEvent.getActionIndex(); + final int eventX = (int) mEvent.getX(pointerIndex); + final int eventY = (int) mEvent.getY(pointerIndex); + Rect bounds = mTempRect; + if (mAms.getAccessibilityFocusBounds(eventX, eventY, bounds) + && !bounds.contains(eventX, eventY)) { + mLongPressingPointerId = mEvent.getPointerId(pointerIndex); + mLongPressingPointerDeltaX = eventX - bounds.centerX(); + mLongPressingPointerDeltaY = eventY - bounds.centerY(); + } else { + mLongPressingPointerId = -1; + mLongPressingPointerDeltaX = 0; + mLongPressingPointerDeltaY = 0; + } + // We are sending events so send exit and gesture + // end since we transition to another state. final int pointerId = mReceivedPointerTracker.getPrimaryActivePointerId(); final int pointerIdBits = (1 << pointerId); - ensureHoverExitSent(mEvent, pointerIdBits, mPolicyFlags); + mAms.touchExplorationGestureEnded(); + sendExitEventsIfNeeded(mPolicyFlags); + mCurrentState = STATE_DELEGATING; sendDownForAllActiveNotInjectedPointers(mEvent, mPolicyFlags); - mTouchExploreGestureInProgress = false; - mLastTouchExploreEvent = null; clear(); } @@ -1047,20 +1286,41 @@ public class TouchExplorer { /** * Class for delayed sending of hover events. */ - private final class SendHoverDelayed implements Runnable { - private MotionEvent mEvent; - private int mAction; + class SendHoverDelayed implements Runnable { + private final String LOG_TAG_SEND_HOVER_DELAYED = SendHoverDelayed.class.getName(); + + private final int mHoverAction; + private final boolean mGestureStarted; + + private MotionEvent mPrototype; private int mPointerIdBits; private int mPolicyFlags; - public void post(MotionEvent prototype, int action, int pointerIdBits, int policyFlags, - long delay) { + public SendHoverDelayed(int hoverAction, boolean gestureStarted) { + mHoverAction = hoverAction; + mGestureStarted = gestureStarted; + } + + public void post(MotionEvent prototype, int pointerIdBits, int policyFlags) { remove(); - mEvent = MotionEvent.obtain(prototype); - mAction = action; + mPrototype = MotionEvent.obtain(prototype); mPointerIdBits = pointerIdBits; mPolicyFlags = policyFlags; - mHandler.postDelayed(this, delay); + mHandler.postDelayed(this, mTapTimeout); + } + + public float getX() { + if (isPending()) { + return mPrototype.getX(); + } + return 0; + } + + public float getY() { + if (isPending()) { + return mPrototype.getY(); + } + return 0; } public void remove() { @@ -1068,23 +1328,22 @@ public class TouchExplorer { clear(); } - private boolean isPenidng() { - return (mEvent != null); + private boolean isPending() { + return (mPrototype != null); } private void clear() { - if (!isPenidng()) { + if (!isPending()) { return; } - mEvent.recycle(); - mEvent = null; - mAction = 0; + mPrototype.recycle(); + mPrototype = null; mPointerIdBits = -1; mPolicyFlags = 0; } public void forceSendAndRemove() { - if (isPenidng()) { + if (isPending()) { run(); remove(); } @@ -1092,16 +1351,17 @@ public class TouchExplorer { public void run() { if (DEBUG) { - if (mAction == MotionEvent.ACTION_HOVER_ENTER) { - Slog.d(LOG_TAG, "Injecting: " + MotionEvent.ACTION_HOVER_ENTER); - } else if (mAction == MotionEvent.ACTION_HOVER_MOVE) { - Slog.d(LOG_TAG, "Injecting: MotionEvent.ACTION_HOVER_MOVE"); - } else if (mAction == MotionEvent.ACTION_HOVER_EXIT) { - Slog.d(LOG_TAG, "Injecting: MotionEvent.ACTION_HOVER_EXIT"); - } + Slog.d(LOG_TAG_SEND_HOVER_DELAYED, "Injecting motion event: " + + MotionEvent.actionToString(mHoverAction)); + Slog.d(LOG_TAG_SEND_HOVER_DELAYED, mGestureStarted ? + "touchExplorationGestureStarted" : "touchExplorationGestureEnded"); } - - sendMotionEvent(mEvent, mAction, mPointerIdBits, mPolicyFlags); + if (mGestureStarted) { + mAms.touchExplorationGestureStarted(); + } else { + mAms.touchExplorationGestureEnded(); + } + sendMotionEvent(mPrototype, mHoverAction, mPointerIdBits, mPolicyFlags); clear(); } } @@ -1120,8 +1380,8 @@ public class TouchExplorer { // The time of the last injected down. private long mLastInjectedDownEventTime; - // The action of the last injected hover event. - private int mLastInjectedHoverEventAction = MotionEvent.ACTION_HOVER_EXIT; + // The last injected hover event. + private MotionEvent mLastInjectedHoverEvent; /** * Processes an injected {@link MotionEvent} event. @@ -1150,11 +1410,14 @@ public class TouchExplorer { case MotionEvent.ACTION_HOVER_ENTER: case MotionEvent.ACTION_HOVER_MOVE: case MotionEvent.ACTION_HOVER_EXIT: { - mLastInjectedHoverEventAction = event.getActionMasked(); + if (mLastInjectedHoverEvent != null) { + mLastInjectedHoverEvent.recycle(); + } + mLastInjectedHoverEvent = MotionEvent.obtain(event); } break; } if (DEBUG) { - Slog.i(LOG_TAG_INJECTED_POINTER_TRACKER, "Injected pointer: " + toString()); + Slog.i(LOG_TAG_INJECTED_POINTER_TRACKER, "Injected pointer:\n" + toString()); } } @@ -1198,10 +1461,10 @@ public class TouchExplorer { } /** - * @return The action of the last injected hover event. + * @return The the last injected hover event. */ - public int getLastInjectedHoverAction() { - return mLastInjectedHoverEventAction; + public MotionEvent getLastInjectedHoverEvent() { + return mLastInjectedHoverEvent; } @Override @@ -1260,6 +1523,8 @@ public class TouchExplorer { private float mLastReceivedUpPointerDownX; private float mLastReceivedUpPointerDownY; + private MotionEvent mLastReceivedEvent; + /** * Creates a new instance. * @@ -1294,6 +1559,11 @@ public class TouchExplorer { * @param event The event to process. */ public void onMotionEvent(MotionEvent event) { + if (mLastReceivedEvent != null) { + mLastReceivedEvent.recycle(); + } + mLastReceivedEvent = MotionEvent.obtain(event); + final int action = event.getActionMasked(); switch (action) { case MotionEvent.ACTION_DOWN: { @@ -1318,6 +1588,13 @@ public class TouchExplorer { } /** + * @return The last received event. + */ + public MotionEvent getLastReceivedEvent() { + return mLastReceivedEvent; + } + + /** * @return The number of received pointers that are down. */ public int getReceivedPointerDownCount() { diff --git a/services/java/com/android/server/wm/WindowManagerService.java b/services/java/com/android/server/wm/WindowManagerService.java index 076ba9a..b89e48f 100755 --- a/services/java/com/android/server/wm/WindowManagerService.java +++ b/services/java/com/android/server/wm/WindowManagerService.java @@ -6561,6 +6561,16 @@ public class WindowManagerService extends IWindowManager.Stub sendScreenStatusToClients(); } + public IBinder getFocusedWindowClientToken() { + synchronized (mWindowMap) { + WindowState windowState = getFocusedWindowLocked(); + if (windowState != null) { + return windowState.mClient.asBinder(); + } + return null; + } + } + private WindowState getFocusedWindow() { synchronized (mWindowMap) { return getFocusedWindowLocked(); |