FileDocCategorySizeDatePackage
InputEventConsistencyVerifier.javaAPI DocAndroid 5.1 API31434Thu Mar 12 22:22:10 GMT 2015android.view

InputEventConsistencyVerifier

public 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.

hide

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_INPUT
Indicates 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.

param
caller The object to which the verifier is attached.
param
flags Flags to the verifier, or 0 if none.


                                  
         
        this(caller, flags, null);
    
public InputEventConsistencyVerifier(Object caller, int flags, String logTag)
Creates an input consistency verifier.

param
caller The object to which the verifier is attached.
param
flags Flags to the verifier, or 0 if none.
param
logTag Tag for logging. If null defaults to the short class name.

        this.mCaller = caller;
        this.mFlags = flags;
        this.mLogTag = (logTag != null) ? logTag : "InputEventConsistencyVerifier";
    
Methods Summary
private voidaddKeyState(int deviceId, int source, int keyCode)

        KeyState state = KeyState.obtain(deviceId, source, keyCode);
        state.next = mKeyStateList;
        mKeyStateList = state;
    
private static voidappendEvent(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 voidensureHistorySizeIsZeroForThisAction(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 voidensureMetaStateIsNormalized(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 voidensurePointerCountIsOneForThisAction(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$KeyStatefindKeyState(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 voidfinishEvent()

        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 booleanisInstrumentationEnabled()
Determines whether the instrumentation should be enabled.

return
True if it should be enabled.

        return IS_ENG_BUILD;
    
public voidonGenericMotionEvent(MotionEvent event, int nestingLevel)
Checks a generic motion event.

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.

        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 voidonInputEvent(InputEvent event, int nestingLevel)
Checks an arbitrary input event.

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.

        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 voidonKeyEvent(KeyEvent event, int nestingLevel)
Checks a key event.

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.

        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 voidonTouchEvent(MotionEvent event, int nestingLevel)
Checks a touch event.

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.

        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 voidonTrackballEvent(MotionEvent event, int nestingLevel)
Checks a trackball event.

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.

        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 voidonUnhandledEvent(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.

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.

        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 voidproblem(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 voidreset()
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 booleanstartEvent(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;