diff options
author | Svetoslav Ganov <svetoslavganov@google.com> | 2012-05-16 15:48:55 -0700 |
---|---|---|
committer | Svetoslav Ganov <svetoslavganov@google.com> | 2012-05-21 14:08:57 -0700 |
commit | e15ccb93add99ebb9cd7aec03a04faa37f45b39d (patch) | |
tree | aa80b8d4fc4099c26246772992411701739bc655 /services/java | |
parent | d51ac09fd7db5d810452138749f9291617c0dbf7 (diff) | |
download | frameworks_base-e15ccb93add99ebb9cd7aec03a04faa37f45b39d.zip frameworks_base-e15ccb93add99ebb9cd7aec03a04faa37f45b39d.tar.gz frameworks_base-e15ccb93add99ebb9cd7aec03a04faa37f45b39d.tar.bz2 |
Changing the interaction model of the touch explorer.
1. Now the user have to double tap to activate the last
item. If the last touched window is not active because
it does not take input focus the click on the last
touch explored location. Othewise the click is on the
accessibility focus location.
bug:5932640
Change-Id: Ibb7b97262a7c5f2f94abef429e02790fdc91a8dd
Diffstat (limited to 'services/java')
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(); |