InputEventConsistencyVerifierpublic final class InputEventConsistencyVerifier extends Object Checks whether a sequence of input events is self-consistent.
Logs a description of each problem detected.
When a problem is detected, the event is tainted. This mechanism prevents the same
error from being reported multiple times.
|
Fields Summary |
---|
private static final boolean | IS_ENG_BUILD | private static final String | EVENT_TYPE_KEY | private static final String | EVENT_TYPE_TRACKBALL | private static final String | EVENT_TYPE_TOUCH | private static final String | EVENT_TYPE_GENERIC_MOTION | private static final int | RECENT_EVENTS_TO_LOG | private final Object | mCaller | private final int | mFlags | private final String | mLogTag | private int | mLastEventSeq | private String | mLastEventType | private int | mLastNestingLevel | private InputEvent[] | mRecentEvents | private boolean[] | mRecentEventsUnhandled | private int | mMostRecentEventIndex | private InputEvent | mCurrentEvent | private String | mCurrentEventType | private KeyState | mKeyStateList | private boolean | mTrackballDown | private boolean | mTrackballUnhandled | private int | mTouchEventStreamPointers | private int | mTouchEventStreamDeviceId | private int | mTouchEventStreamSource | private boolean | mTouchEventStreamIsTainted | private boolean | mTouchEventStreamUnhandled | private boolean | mHoverEntered | private StringBuilder | mViolationMessage | public static final int | FLAG_RAW_DEVICE_INPUTIndicates that the verifier is intended to act on raw device input event streams.
Disables certain checks for invariants that are established by the input dispatcher
itself as it delivers input events, such as key repeating behavior. |
Constructors Summary |
---|
public InputEventConsistencyVerifier(Object caller, int flags)Creates an input consistency verifier.
this(caller, flags, null);
| public InputEventConsistencyVerifier(Object caller, int flags, String logTag)Creates an input consistency verifier.
this.mCaller = caller;
this.mFlags = flags;
this.mLogTag = (logTag != null) ? logTag : "InputEventConsistencyVerifier";
|
Methods Summary |
---|
private void | addKeyState(int deviceId, int source, int keyCode)
KeyState state = KeyState.obtain(deviceId, source, keyCode);
state.next = mKeyStateList;
mKeyStateList = state;
| private static void | appendEvent(java.lang.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 | ensureHistorySizeIsZeroForThisAction(MotionEvent event)
final int historySize = event.getHistorySize();
if (historySize != 0) {
problem("History size is " + historySize + " but it should always be 0 for "
+ MotionEvent.actionToString(event.getAction()));
}
| private void | ensureMetaStateIsNormalized(int metaState)
final int normalizedMetaState = KeyEvent.normalizeMetaState(metaState);
if (normalizedMetaState != metaState) {
problem(String.format("Metastate not normalized. Was 0x%08x but expected 0x%08x.",
metaState, normalizedMetaState));
}
| private void | ensurePointerCountIsOneForThisAction(MotionEvent event)
final int pointerCount = event.getPointerCount();
if (pointerCount != 1) {
problem("Pointer count is " + pointerCount + " but it should always be 1 for "
+ MotionEvent.actionToString(event.getAction()));
}
| private android.view.InputEventConsistencyVerifier$KeyState | findKeyState(int deviceId, int source, int keyCode, boolean remove)
KeyState last = null;
KeyState state = mKeyStateList;
while (state != null) {
if (state.deviceId == deviceId && state.source == source
&& state.keyCode == keyCode) {
if (remove) {
if (last != null) {
last.next = state.next;
} else {
mKeyStateList = state.next;
}
state.next = null;
}
return state;
}
last = state;
state = state.next;
}
return null;
| private void | finishEvent()
if (mViolationMessage != null && mViolationMessage.length() != 0) {
if (!mCurrentEvent.isTainted()) {
// Write a log message only if the event was not already tainted.
mViolationMessage.append("\n in ").append(mCaller);
mViolationMessage.append("\n ");
appendEvent(mViolationMessage, 0, mCurrentEvent, false);
if (RECENT_EVENTS_TO_LOG != 0 && mRecentEvents != null) {
mViolationMessage.append("\n -- recent events --");
for (int i = 0; i < RECENT_EVENTS_TO_LOG; i++) {
final int index = (mMostRecentEventIndex + RECENT_EVENTS_TO_LOG - i)
% RECENT_EVENTS_TO_LOG;
final InputEvent event = mRecentEvents[index];
if (event == null) {
break;
}
mViolationMessage.append("\n ");
appendEvent(mViolationMessage, i + 1, event, mRecentEventsUnhandled[index]);
}
}
Log.d(mLogTag, mViolationMessage.toString());
// Taint the event so that we do not generate additional violations from it
// further downstream.
mCurrentEvent.setTainted(true);
}
mViolationMessage.setLength(0);
}
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;
if (mRecentEvents[index] != null) {
mRecentEvents[index].recycle();
}
mRecentEvents[index] = mCurrentEvent.copy();
mRecentEventsUnhandled[index] = false;
}
mCurrentEvent = null;
mCurrentEventType = null;
| public static boolean | isInstrumentationEnabled()Determines whether the instrumentation should be enabled.
return IS_ENG_BUILD;
| public void | onGenericMotionEvent(MotionEvent event, int nestingLevel)Checks a generic motion event.
if (!startEvent(event, nestingLevel, EVENT_TYPE_GENERIC_MOTION)) {
return;
}
try {
ensureMetaStateIsNormalized(event.getMetaState());
final int action = event.getAction();
final int source = event.getSource();
if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) {
switch (action) {
case MotionEvent.ACTION_HOVER_ENTER:
ensurePointerCountIsOneForThisAction(event);
mHoverEntered = true;
break;
case MotionEvent.ACTION_HOVER_MOVE:
ensurePointerCountIsOneForThisAction(event);
break;
case MotionEvent.ACTION_HOVER_EXIT:
ensurePointerCountIsOneForThisAction(event);
if (!mHoverEntered) {
problem("ACTION_HOVER_EXIT without prior ACTION_HOVER_ENTER");
}
mHoverEntered = false;
break;
case MotionEvent.ACTION_SCROLL:
ensureHistorySizeIsZeroForThisAction(event);
ensurePointerCountIsOneForThisAction(event);
break;
default:
problem("Invalid action for generic pointer event.");
break;
}
} else if ((source & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) {
switch (action) {
case MotionEvent.ACTION_MOVE:
ensurePointerCountIsOneForThisAction(event);
break;
default:
problem("Invalid action for generic joystick event.");
break;
}
}
} finally {
finishEvent();
}
| public void | onInputEvent(InputEvent event, int nestingLevel)Checks an arbitrary input event.
if (event instanceof KeyEvent) {
final KeyEvent keyEvent = (KeyEvent)event;
onKeyEvent(keyEvent, nestingLevel);
} else {
final MotionEvent motionEvent = (MotionEvent)event;
if (motionEvent.isTouchEvent()) {
onTouchEvent(motionEvent, nestingLevel);
} else if ((motionEvent.getSource() & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) {
onTrackballEvent(motionEvent, nestingLevel);
} else {
onGenericMotionEvent(motionEvent, nestingLevel);
}
}
| public void | onKeyEvent(KeyEvent event, int nestingLevel)Checks a key event.
if (!startEvent(event, nestingLevel, EVENT_TYPE_KEY)) {
return;
}
try {
ensureMetaStateIsNormalized(event.getMetaState());
final int action = event.getAction();
final int deviceId = event.getDeviceId();
final int source = event.getSource();
final int keyCode = event.getKeyCode();
switch (action) {
case KeyEvent.ACTION_DOWN: {
KeyState state = findKeyState(deviceId, source, keyCode, /*remove*/ false);
if (state != null) {
// If the key is already down, ensure it is a repeat.
// 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 (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.");
}
} else {
addKeyState(deviceId, source, keyCode);
}
break;
}
case KeyEvent.ACTION_UP: {
KeyState state = findKeyState(deviceId, source, keyCode, /*remove*/ true);
if (state == null) {
problem("ACTION_UP but key was not down.");
} else {
state.recycle();
}
break;
}
case KeyEvent.ACTION_MULTIPLE:
break;
default:
problem("Invalid action " + KeyEvent.actionToString(action)
+ " for key event.");
break;
}
} finally {
finishEvent();
}
| public void | onTouchEvent(MotionEvent event, int nestingLevel)Checks a touch event.
if (!startEvent(event, nestingLevel, EVENT_TYPE_TOUCH)) {
return;
}
final int action = event.getAction();
final boolean newStream = action == MotionEvent.ACTION_DOWN
|| action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_OUTSIDE;
if (newStream && (mTouchEventStreamIsTainted || mTouchEventStreamUnhandled)) {
mTouchEventStreamIsTainted = false;
mTouchEventStreamUnhandled = false;
mTouchEventStreamPointers = 0;
}
if (mTouchEventStreamIsTainted) {
event.setTainted(true);
}
try {
ensureMetaStateIsNormalized(event.getMetaState());
final int deviceId = event.getDeviceId();
final int source = event.getSource();
if (!newStream && mTouchEventStreamDeviceId != -1
&& (mTouchEventStreamDeviceId != deviceId
|| mTouchEventStreamSource != source)) {
problem("Touch event stream contains events from multiple sources: "
+ "previous device id " + mTouchEventStreamDeviceId
+ ", previous source " + Integer.toHexString(mTouchEventStreamSource)
+ ", new device id " + deviceId
+ ", new source " + Integer.toHexString(source));
}
mTouchEventStreamDeviceId = deviceId;
mTouchEventStreamSource = source;
final int pointerCount = event.getPointerCount();
if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) {
switch (action) {
case MotionEvent.ACTION_DOWN:
if (mTouchEventStreamPointers != 0) {
problem("ACTION_DOWN but pointers are already down. "
+ "Probably missing ACTION_UP from previous gesture.");
}
ensureHistorySizeIsZeroForThisAction(event);
ensurePointerCountIsOneForThisAction(event);
mTouchEventStreamPointers = 1 << event.getPointerId(0);
break;
case MotionEvent.ACTION_UP:
ensureHistorySizeIsZeroForThisAction(event);
ensurePointerCountIsOneForThisAction(event);
mTouchEventStreamPointers = 0;
mTouchEventStreamIsTainted = false;
break;
case MotionEvent.ACTION_MOVE: {
final int expectedPointerCount =
Integer.bitCount(mTouchEventStreamPointers);
if (pointerCount != expectedPointerCount) {
problem("ACTION_MOVE contained " + pointerCount
+ " pointers but there are currently "
+ expectedPointerCount + " pointers down.");
mTouchEventStreamIsTainted = true;
}
break;
}
case MotionEvent.ACTION_CANCEL:
mTouchEventStreamPointers = 0;
mTouchEventStreamIsTainted = false;
break;
case MotionEvent.ACTION_OUTSIDE:
if (mTouchEventStreamPointers != 0) {
problem("ACTION_OUTSIDE but pointers are still down.");
}
ensureHistorySizeIsZeroForThisAction(event);
ensurePointerCountIsOneForThisAction(event);
mTouchEventStreamIsTainted = false;
break;
default: {
final int actionMasked = event.getActionMasked();
final int actionIndex = event.getActionIndex();
if (actionMasked == MotionEvent.ACTION_POINTER_DOWN) {
if (mTouchEventStreamPointers == 0) {
problem("ACTION_POINTER_DOWN but no other pointers were down.");
mTouchEventStreamIsTainted = true;
}
if (actionIndex < 0 || actionIndex >= pointerCount) {
problem("ACTION_POINTER_DOWN index is " + actionIndex
+ " but the pointer count is " + pointerCount + ".");
mTouchEventStreamIsTainted = true;
} else {
final int id = event.getPointerId(actionIndex);
final int idBit = 1 << id;
if ((mTouchEventStreamPointers & idBit) != 0) {
problem("ACTION_POINTER_DOWN specified pointer id " + id
+ " which is already down.");
mTouchEventStreamIsTainted = true;
} else {
mTouchEventStreamPointers |= idBit;
}
}
ensureHistorySizeIsZeroForThisAction(event);
} else if (actionMasked == MotionEvent.ACTION_POINTER_UP) {
if (actionIndex < 0 || actionIndex >= pointerCount) {
problem("ACTION_POINTER_UP index is " + actionIndex
+ " but the pointer count is " + pointerCount + ".");
mTouchEventStreamIsTainted = true;
} else {
final int id = event.getPointerId(actionIndex);
final int idBit = 1 << id;
if ((mTouchEventStreamPointers & idBit) == 0) {
problem("ACTION_POINTER_UP specified pointer id " + id
+ " which is not currently down.");
mTouchEventStreamIsTainted = true;
} else {
mTouchEventStreamPointers &= ~idBit;
}
}
ensureHistorySizeIsZeroForThisAction(event);
} else {
problem("Invalid action " + MotionEvent.actionToString(action)
+ " for touch event.");
}
break;
}
}
} else {
problem("Source was not SOURCE_CLASS_POINTER.");
}
} finally {
finishEvent();
}
| public void | onTrackballEvent(MotionEvent event, int nestingLevel)Checks a trackball event.
if (!startEvent(event, nestingLevel, EVENT_TYPE_TRACKBALL)) {
return;
}
try {
ensureMetaStateIsNormalized(event.getMetaState());
final int action = event.getAction();
final int source = event.getSource();
if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) {
switch (action) {
case MotionEvent.ACTION_DOWN:
if (mTrackballDown && !mTrackballUnhandled) {
problem("ACTION_DOWN but trackball is already down.");
} else {
mTrackballDown = true;
mTrackballUnhandled = false;
}
ensureHistorySizeIsZeroForThisAction(event);
ensurePointerCountIsOneForThisAction(event);
break;
case MotionEvent.ACTION_UP:
if (!mTrackballDown) {
problem("ACTION_UP but trackball is not down.");
} else {
mTrackballDown = false;
mTrackballUnhandled = false;
}
ensureHistorySizeIsZeroForThisAction(event);
ensurePointerCountIsOneForThisAction(event);
break;
case MotionEvent.ACTION_MOVE:
ensurePointerCountIsOneForThisAction(event);
break;
default:
problem("Invalid action " + MotionEvent.actionToString(action)
+ " for trackball event.");
break;
}
if (mTrackballDown && event.getPressure() <= 0) {
problem("Trackball is down but pressure is not greater than 0.");
} else if (!mTrackballDown && event.getPressure() != 0) {
problem("Trackball is up but pressure is not equal to 0.");
}
} else {
problem("Source was not SOURCE_CLASS_TRACKBALL.");
}
} finally {
finishEvent();
}
| public void | onUnhandledEvent(InputEvent event, int nestingLevel)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.
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 | problem(java.lang.String message)
if (mViolationMessage == null) {
mViolationMessage = new StringBuilder();
}
if (mViolationMessage.length() == 0) {
mViolationMessage.append(mCurrentEventType).append(": ");
} else {
mViolationMessage.append("\n ");
}
mViolationMessage.append(message);
| public void | reset()Resets the state of the input event consistency verifier.
mLastEventSeq = -1;
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();
}
| private boolean | startEvent(InputEvent event, int nestingLevel, java.lang.String eventType)
// Ignore the event if we already checked it at a higher nesting level.
final int seq = event.getSequenceNumber();
if (seq == mLastEventSeq && nestingLevel < mLastNestingLevel
&& eventType == mLastEventType) {
return false;
}
if (nestingLevel > 0) {
mLastEventSeq = seq;
mLastEventType = eventType;
mLastNestingLevel = nestingLevel;
} else {
mLastEventSeq = -1;
mLastEventType = null;
mLastNestingLevel = 0;
}
mCurrentEvent = event;
mCurrentEventType = eventType;
return true;
|
|