Track unhandled input events in consistency verifiers.

This fixes spurious verification errors that would be generated
when a view declined an initial event such as ACTION_DOWN.  Since
the view would not receive the rest of the event stream, it would
not see the corresponding ACTION_UP and the next ACTION_DOWN would
trigger a spurious verification error.

Change-Id: I2386acf378cd1765d5446faed5ad9c6525f8b400
This commit is contained in:
Jeff Brown
2011-04-19 23:46:52 -07:00
parent 79ac969d7a
commit bbdc50b102
5 changed files with 292 additions and 158 deletions

View File

@@ -590,8 +590,14 @@ public class GestureDetector {
mHandler.removeMessages(SHOW_PRESS);
mHandler.removeMessages(LONG_PRESS);
break;
case MotionEvent.ACTION_CANCEL:
cancel();
break;
}
if (!handled && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(ev, 0);
}
return handled;
}

View File

@@ -54,6 +54,7 @@ public final class InputEventConsistencyVerifier {
// Copy of the most recent events.
private InputEvent[] mRecentEvents;
private boolean[] mRecentEventsUnhandled;
private int mMostRecentEventIndex;
// Current event and its type.
@@ -65,6 +66,7 @@ public final class InputEventConsistencyVerifier {
// Current state of the trackball.
private boolean mTrackballDown;
private boolean mTrackballUnhandled;
// Bitfield of pointer ids that are currently down.
// Assumes that the largest possible pointer id is 31, which is potentially subject to change.
@@ -79,6 +81,9 @@ public final class InputEventConsistencyVerifier {
// Reset on down or cancel.
private boolean mTouchEventStreamIsTainted;
// Set to true if the touch event stream is partially unhandled.
private boolean mTouchEventStreamUnhandled;
// Set to true if we received hover enter.
private boolean mHoverEntered;
@@ -117,9 +122,17 @@ public final class InputEventConsistencyVerifier {
mLastEvent = null;
mLastNestingLevel = 0;
mTrackballDown = false;
mTrackballUnhandled = false;
mTouchEventStreamPointers = 0;
mTouchEventStreamIsTainted = false;
mTouchEventStreamUnhandled = false;
mHoverEntered = false;
while (mKeyStateList != null) {
final KeyState state = mKeyStateList;
mKeyStateList = state.next;
state.recycle();
}
}
/**
@@ -176,7 +189,9 @@ public final class InputEventConsistencyVerifier {
// We don't perform this check when processing raw device input
// because the input dispatcher itself is responsible for setting
// the key repeat count before it delivers input events.
if ((mFlags & FLAG_RAW_DEVICE_INPUT) == 0
if (state.unhandled) {
state.unhandled = false;
} else if ((mFlags & FLAG_RAW_DEVICE_INPUT) == 0
&& event.getRepeatCount() == 0) {
problem("ACTION_DOWN but key is already down and this event "
+ "is not a key repeat.");
@@ -229,10 +244,11 @@ public final class InputEventConsistencyVerifier {
if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) {
switch (action) {
case MotionEvent.ACTION_DOWN:
if (mTrackballDown) {
if (mTrackballDown && !mTrackballUnhandled) {
problem("ACTION_DOWN but trackball is already down.");
} else {
mTrackballDown = true;
mTrackballUnhandled = false;
}
ensureHistorySizeIsZeroForThisAction(event);
ensurePointerCountIsOneForThisAction(event);
@@ -242,6 +258,7 @@ public final class InputEventConsistencyVerifier {
problem("ACTION_UP but trackball is not down.");
} else {
mTrackballDown = false;
mTrackballUnhandled = false;
}
ensureHistorySizeIsZeroForThisAction(event);
ensurePointerCountIsOneForThisAction(event);
@@ -285,11 +302,13 @@ public final class InputEventConsistencyVerifier {
final int action = event.getAction();
final boolean newStream = action == MotionEvent.ACTION_DOWN
|| action == MotionEvent.ACTION_CANCEL;
if (mTouchEventStreamIsTainted) {
if (mTouchEventStreamIsTainted || mTouchEventStreamUnhandled) {
if (newStream) {
mTouchEventStreamIsTainted = false;
mTouchEventStreamUnhandled = false;
mTouchEventStreamPointers = 0;
} else {
finishEvent(true);
finishEvent(mTouchEventStreamIsTainted);
return;
}
}
@@ -467,6 +486,48 @@ public final class InputEventConsistencyVerifier {
}
}
/**
* Notifies the verifier that a given event was unhandled and the rest of the
* trace for the event should be ignored.
* This method should only be called if the event was previously checked by
* the consistency verifier using {@link #onInputEvent} and other methods.
* @param event The event.
* @param nestingLevel The nesting level: 0 if called from the base class,
* or 1 from a subclass. If the event was already checked by this consistency verifier
* at a higher nesting level, it will not be checked again. Used to handle the situation
* where a subclass dispatching method delegates to its superclass's dispatching method
* and both dispatching methods call into the consistency verifier.
*/
public void onUnhandledEvent(InputEvent event, int nestingLevel) {
if (nestingLevel != mLastNestingLevel) {
return;
}
if (mRecentEventsUnhandled != null) {
mRecentEventsUnhandled[mMostRecentEventIndex] = true;
}
if (event instanceof KeyEvent) {
final KeyEvent keyEvent = (KeyEvent)event;
final int deviceId = keyEvent.getDeviceId();
final int source = keyEvent.getSource();
final int keyCode = keyEvent.getKeyCode();
final KeyState state = findKeyState(deviceId, source, keyCode, /*remove*/ false);
if (state != null) {
state.unhandled = true;
}
} else {
final MotionEvent motionEvent = (MotionEvent)event;
if (motionEvent.isTouchEvent()) {
mTouchEventStreamUnhandled = true;
} else if ((motionEvent.getSource() & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) {
if (mTrackballDown) {
mTrackballUnhandled = true;
}
}
}
}
private void ensureMetaStateIsNormalized(int metaState) {
final int normalizedMetaState = KeyEvent.normalizeMetaState(metaState);
if (normalizedMetaState != metaState) {
@@ -518,7 +579,8 @@ public final class InputEventConsistencyVerifier {
private void finishEvent(boolean tainted) {
if (mViolationMessage != null && mViolationMessage.length() != 0) {
mViolationMessage.append("\n in ").append(mCaller);
mViolationMessage.append("\n ").append(mCurrentEvent);
mViolationMessage.append("\n ");
appendEvent(mViolationMessage, 0, mCurrentEvent, false);
if (RECENT_EVENTS_TO_LOG != 0 && mRecentEvents != null) {
mViolationMessage.append("\n -- recent events --");
@@ -529,7 +591,8 @@ public final class InputEventConsistencyVerifier {
if (event == null) {
break;
}
mViolationMessage.append("\n ").append(i + 1).append(": ").append(event);
mViolationMessage.append("\n ");
appendEvent(mViolationMessage, i + 1, event, mRecentEventsUnhandled[index]);
}
}
@@ -547,6 +610,7 @@ public final class InputEventConsistencyVerifier {
if (RECENT_EVENTS_TO_LOG != 0) {
if (mRecentEvents == null) {
mRecentEvents = new InputEvent[RECENT_EVENTS_TO_LOG];
mRecentEventsUnhandled = new boolean[RECENT_EVENTS_TO_LOG];
}
final int index = (mMostRecentEventIndex + 1) % RECENT_EVENTS_TO_LOG;
mMostRecentEventIndex = index;
@@ -554,12 +618,23 @@ public final class InputEventConsistencyVerifier {
mRecentEvents[index].recycle();
}
mRecentEvents[index] = mCurrentEvent.copy();
mRecentEventsUnhandled[index] = false;
}
mCurrentEvent = null;
mCurrentEventType = null;
}
private static void appendEvent(StringBuilder message, int index,
InputEvent event, boolean unhandled) {
message.append(index).append(": sent at ").append(event.getEventTimeNano());
message.append(", ");
if (unhandled) {
message.append("(unhandled) ");
}
message.append(event);
}
private void problem(String message) {
if (mViolationMessage == null) {
mViolationMessage = new StringBuilder();
@@ -608,6 +683,7 @@ public final class InputEventConsistencyVerifier {
public int deviceId;
public int source;
public int keyCode;
public boolean unhandled;
private KeyState() {
}
@@ -625,6 +701,7 @@ public final class InputEventConsistencyVerifier {
state.deviceId = deviceId;
state.source = source;
state.keyCode = keyCode;
state.unhandled = false;
return state;
}

View File

@@ -183,15 +183,15 @@ public class ScaleGestureDetector {
}
final int action = event.getActionMasked();
boolean handled = true;
if (action == MotionEvent.ACTION_DOWN) {
reset(); // Start fresh
}
if (mInvalidGesture) return false;
if (!mGestureInProgress) {
boolean handled = true;
if (mInvalidGesture) {
handled = false;
} else if (!mGestureInProgress) {
switch (action) {
case MotionEvent.ACTION_DOWN: {
mActiveId0 = event.getPointerId(0);
@@ -467,6 +467,10 @@ public class ScaleGestureDetector {
break;
}
}
if (!handled && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}
return handled;
}

View File

@@ -4614,8 +4614,15 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility
return true;
}
return event.dispatch(this, mAttachInfo != null
? mAttachInfo.mKeyDispatchState : null, this);
if (event.dispatch(this, mAttachInfo != null
? mAttachInfo.mKeyDispatchState : null, this)) {
return true;
}
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}
return false;
}
/**
@@ -4640,16 +4647,22 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility
mInputEventConsistencyVerifier.onTouchEvent(event, 0);
}
if (!onFilterTouchEventForSecurity(event)) {
return false;
if (onFilterTouchEventForSecurity(event)) {
//noinspection SimplifiableIfStatement
if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
mOnTouchListener.onTouch(this, event)) {
return true;
}
if (onTouchEvent(event)) {
return true;
}
}
//noinspection SimplifiableIfStatement
if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
mOnTouchListener.onTouch(this, event)) {
return true;
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}
return onTouchEvent(event);
return false;
}
/**
@@ -4682,7 +4695,14 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility
}
//Log.i("view", "view=" + this + ", " + event.toString());
return onTrackballEvent(event);
if (onTrackballEvent(event)) {
return true;
}
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}
return false;
}
/**
@@ -4723,7 +4743,15 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility
&& mOnGenericMotionListener.onGenericMotion(this, event)) {
return true;
}
return onGenericMotionEvent(event);
if (onGenericMotionEvent(event)) {
return true;
}
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}
return false;
}
/**

View File

@@ -1131,9 +1131,17 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
}
if ((mPrivateFlags & (FOCUSED | HAS_BOUNDS)) == (FOCUSED | HAS_BOUNDS)) {
return super.dispatchKeyEvent(event);
if (super.dispatchKeyEvent(event)) {
return true;
}
} else if (mFocused != null && (mFocused.mPrivateFlags & HAS_BOUNDS) == HAS_BOUNDS) {
return mFocused.dispatchKeyEvent(event);
if (mFocused.dispatchKeyEvent(event)) {
return true;
}
}
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 1);
}
return false;
}
@@ -1161,9 +1169,17 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
}
if ((mPrivateFlags & (FOCUSED | HAS_BOUNDS)) == (FOCUSED | HAS_BOUNDS)) {
return super.dispatchTrackballEvent(event);
if (super.dispatchTrackballEvent(event)) {
return true;
}
} else if (mFocused != null && (mFocused.mPrivateFlags & HAS_BOUNDS) == HAS_BOUNDS) {
return mFocused.dispatchTrackballEvent(event);
if (mFocused.dispatchTrackballEvent(event)) {
return true;
}
}
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 1);
}
return false;
}
@@ -1344,155 +1360,158 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
}
if (!onFilterTouchEventForSecurity(ev)) {
return false;
}
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Throw away all previous state when starting a new touch gesture.
// The framework may have dropped the up or cancel event for the previous gesture
// due to an app switch, ANR, or some other state change.
cancelAndClearTouchTargets(ev);
resetTouchState();
}
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case onInterceptTouchEvent() changed it
} else {
intercepted = false;
// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Throw away all previous state when starting a new touch gesture.
// The framework may have dropped the up or cancel event for the previous gesture
// due to an app switch, ANR, or some other state change.
cancelAndClearTouchTargets(ev);
resetTouchState();
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
// Check for cancelation.
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
// Update list of touch targets for pointer down, if needed.
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) {
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
final int actionIndex = ev.getActionIndex(); // always 0 for down
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.ALL_POINTER_IDS;
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
// Clean up earlier touch targets for this pointer id in case they
// have become out of sync.
removePointersFromTouchTargets(idBitsToAssign);
// Check for cancelation.
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
final int childrenCount = mChildrenCount;
if (childrenCount != 0) {
// Find a child that can receive the event. Scan children from front to back.
final View[] children = mChildren;
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// Update list of touch targets for pointer down, if needed.
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) {
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
final int actionIndex = ev.getActionIndex(); // always 0 for down
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.ALL_POINTER_IDS;
for (int i = childrenCount - 1; i >= 0; i--) {
final View child = children[i];
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
// Clean up earlier touch targets for this pointer id in case they
// have become out of sync.
removePointersFromTouchTargets(idBitsToAssign);
final int childrenCount = mChildrenCount;
if (childrenCount != 0) {
// Find a child that can receive the event.
// Scan children from front to back.
final View[] children = mChildren;
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
for (int i = childrenCount - 1; i >= 0; i--) {
final View child = children[i];
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
continue;
}
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// Child is already receiving touch within its bounds.
// Give it the new pointer in addition to the ones it is handling.
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
mLastTouchDownIndex = i;
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
}
if (newTouchTarget == null && mFirstTouchTarget != null) {
// Did not find a child to receive the event.
// Assign the pointer to the least recently added target.
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
}
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// Dispatch to touch targets, excluding the new touch target if we already
// dispatched to it. Cancel touch targets if necessary.
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// Child is already receiving touch within its bounds.
// Give it the new pointer in addition to the ones it is handling.
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
mLastTouchDownIndex = i;
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
predecessor = target;
target = next;
}
}
if (newTouchTarget == null && mFirstTouchTarget != null) {
// Did not find a child to receive the event.
// Assign the pointer to the least recently added target.
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
// Update list of touch targets for pointer up or cancel, if needed.
if (canceled
|| actionMasked == MotionEvent.ACTION_UP
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
resetTouchState();
} else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
final int actionIndex = ev.getActionIndex();
final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
removePointersFromTouchTargets(idBitsToRemove);
}
}
// Dispatch to touch targets.
boolean handled = false;
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// Dispatch to touch targets, excluding the new touch target if we already
// dispatched to it. Cancel touch targets if necessary.
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
if (!handled && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
}
// Update list of touch targets for pointer up or cancel, if needed.
if (canceled
|| actionMasked == MotionEvent.ACTION_UP
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
resetTouchState();
} else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
final int actionIndex = ev.getActionIndex();
final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
removePointersFromTouchTargets(idBitsToRemove);
}
return handled;
}