diff options
author | Justin Klaassen <justinklaassen@google.com> | 2017-09-15 17:58:39 -0400 |
---|---|---|
committer | Justin Klaassen <justinklaassen@google.com> | 2017-09-15 17:58:39 -0400 |
commit | 10d07c88d69cc64f73a069163e7ea5ba2519a099 (patch) | |
tree | 8dbd149eb350320a29c3d10e7ad3201de1c5cbee /android/view/InputEventConsistencyVerifier.java | |
parent | 677516fb6b6f207d373984757d3d9450474b6b00 (diff) | |
download | android-28-10d07c88d69cc64f73a069163e7ea5ba2519a099.tar.gz |
Import Android SDK Platform PI [4335822]
/google/data/ro/projects/android/fetch_artifact \
--bid 4335822 \
--target sdk_phone_armv7-win_sdk \
sdk-repo-linux-sources-4335822.zip
AndroidVersion.ApiLevel has been modified to appear as 28
Change-Id: Ic8f04be005a71c2b9abeaac754d8da8d6f9a2c32
Diffstat (limited to 'android/view/InputEventConsistencyVerifier.java')
-rw-r--r-- | android/view/InputEventConsistencyVerifier.java | 804 |
1 files changed, 804 insertions, 0 deletions
diff --git a/android/view/InputEventConsistencyVerifier.java b/android/view/InputEventConsistencyVerifier.java new file mode 100644 index 00000000..7e8ec046 --- /dev/null +++ b/android/view/InputEventConsistencyVerifier.java @@ -0,0 +1,804 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +import android.os.Build; +import android.util.Log; + +/** + * Checks whether a sequence of input events is self-consistent. + * Logs a description of each problem detected. + * <p> + * When a problem is detected, the event is tainted. This mechanism prevents the same + * error from being reported multiple times. + * </p> + * + * @hide + */ +public final class InputEventConsistencyVerifier { + private static final boolean IS_ENG_BUILD = Build.IS_ENG; + + private static final String EVENT_TYPE_KEY = "KeyEvent"; + private static final String EVENT_TYPE_TRACKBALL = "TrackballEvent"; + private static final String EVENT_TYPE_TOUCH = "TouchEvent"; + private static final String EVENT_TYPE_GENERIC_MOTION = "GenericMotionEvent"; + + // The number of recent events to log when a problem is detected. + // Can be set to 0 to disable logging recent events but the runtime overhead of + // this feature is negligible on current hardware. + private static final int RECENT_EVENTS_TO_LOG = 5; + + // The object to which the verifier is attached. + private final Object mCaller; + + // Consistency verifier flags. + private final int mFlags; + + // Tag for logging which a client can set to help distinguish the output + // from different verifiers since several can be active at the same time. + // If not provided defaults to the simple class name. + private final String mLogTag; + + // The most recently checked event and the nesting level at which it was checked. + // This is only set when the verifier is called from a nesting level greater than 0 + // so that the verifier can detect when it has been asked to verify the same event twice. + // It does not make sense to examine the contents of the last event since it may have + // been recycled. + private int mLastEventSeq; + private String mLastEventType; + private int mLastNestingLevel; + + // Copy of the most recent events. + private InputEvent[] mRecentEvents; + private boolean[] mRecentEventsUnhandled; + private int mMostRecentEventIndex; + + // Current event and its type. + private InputEvent mCurrentEvent; + private String mCurrentEventType; + + // Linked list of key state objects. + private KeyState mKeyStateList; + + // 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. + // (See MAX_POINTER_ID in frameworks/base/include/ui/Input.h) + private int mTouchEventStreamPointers; + + // The device id and source of the current stream of touch events. + private int mTouchEventStreamDeviceId = -1; + private int mTouchEventStreamSource; + + // Set to true when we discover that the touch event stream is inconsistent. + // 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; + + // The bitset of buttons which we've received ACTION_BUTTON_PRESS for. + private int mButtonsPressed; + + // The current violation message. + private StringBuilder mViolationMessage; + + /** + * 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. + */ + public static final int FLAG_RAW_DEVICE_INPUT = 1 << 0; + + /** + * 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. + */ + public InputEventConsistencyVerifier(Object caller, int flags) { + this(caller, flags, null); + } + + /** + * 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. + */ + public InputEventConsistencyVerifier(Object caller, int flags, String logTag) { + this.mCaller = caller; + this.mFlags = flags; + this.mLogTag = (logTag != null) ? logTag : "InputEventConsistencyVerifier"; + } + + /** + * Determines whether the instrumentation should be enabled. + * @return True if it should be enabled. + */ + public static boolean isInstrumentationEnabled() { + return IS_ENG_BUILD; + } + + /** + * Resets the state of the input event consistency verifier. + */ + public void reset() { + mLastEventSeq = -1; + mLastNestingLevel = 0; + mTrackballDown = false; + mTrackballUnhandled = false; + mTouchEventStreamPointers = 0; + mTouchEventStreamIsTainted = false; + mTouchEventStreamUnhandled = false; + mHoverEntered = false; + mButtonsPressed = 0; + + while (mKeyStateList != null) { + final KeyState state = mKeyStateList; + mKeyStateList = state.next; + state.recycle(); + } + } + + /** + * 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. + */ + public void onInputEvent(InputEvent event, int nestingLevel) { + 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); + } + } + } + + /** + * 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. + */ + public void onKeyEvent(KeyEvent event, int nestingLevel) { + 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(); + } + } + + /** + * 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. + */ + public void onTrackballEvent(MotionEvent event, int nestingLevel) { + 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(); + } + } + + /** + * 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. + */ + public void onTouchEvent(MotionEvent event, int nestingLevel) { + 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(); + } + } + + /** + * 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. + */ + public void onGenericMotionEvent(MotionEvent event, int nestingLevel) { + if (!startEvent(event, nestingLevel, EVENT_TYPE_GENERIC_MOTION)) { + return; + } + + try { + ensureMetaStateIsNormalized(event.getMetaState()); + + final int action = event.getAction(); + final int source = event.getSource(); + final int buttonState = event.getButtonState(); + final int actionButton = event.getActionButton(); + 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; + case MotionEvent.ACTION_BUTTON_PRESS: + ensureActionButtonIsNonZeroForThisAction(event); + if ((mButtonsPressed & actionButton) != 0) { + problem("Action button for ACTION_BUTTON_PRESS event is " + + actionButton + ", but it has already been pressed and " + + "has yet to be released."); + } + + mButtonsPressed |= actionButton; + // The system will automatically mirror the stylus buttons onto the button + // state as the old set of generic buttons for apps targeting pre-M. If + // it looks this has happened, go ahead and set the generic buttons as + // pressed to prevent spurious errors. + if (actionButton == MotionEvent.BUTTON_STYLUS_PRIMARY && + (buttonState & MotionEvent.BUTTON_SECONDARY) != 0) { + mButtonsPressed |= MotionEvent.BUTTON_SECONDARY; + } else if (actionButton == MotionEvent.BUTTON_STYLUS_SECONDARY && + (buttonState & MotionEvent.BUTTON_TERTIARY) != 0) { + mButtonsPressed |= MotionEvent.BUTTON_TERTIARY; + } + + if (mButtonsPressed != buttonState) { + problem(String.format("Reported button state differs from " + + "expected button state based on press and release events. " + + "Is 0x%08x but expected 0x%08x.", + buttonState, mButtonsPressed)); + } + break; + case MotionEvent.ACTION_BUTTON_RELEASE: + ensureActionButtonIsNonZeroForThisAction(event); + if ((mButtonsPressed & actionButton) != actionButton) { + problem("Action button for ACTION_BUTTON_RELEASE event is " + + actionButton + ", but it was either never pressed or has " + + "already been released."); + } + + mButtonsPressed &= ~actionButton; + // The system will automatically mirror the stylus buttons onto the button + // state as the old set of generic buttons for apps targeting pre-M. If + // it looks this has happened, go ahead and set the generic buttons as + // released to prevent spurious errors. + if (actionButton == MotionEvent.BUTTON_STYLUS_PRIMARY && + (buttonState & MotionEvent.BUTTON_SECONDARY) == 0) { + mButtonsPressed &= ~MotionEvent.BUTTON_SECONDARY; + } else if (actionButton == MotionEvent.BUTTON_STYLUS_SECONDARY && + (buttonState & MotionEvent.BUTTON_TERTIARY) == 0) { + mButtonsPressed &= ~MotionEvent.BUTTON_TERTIARY; + } + + if (mButtonsPressed != buttonState) { + problem(String.format("Reported button state differs from " + + "expected button state based on press and release events. " + + "Is 0x%08x but expected 0x%08x.", + buttonState, mButtonsPressed)); + } + 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(); + } + } + + /** + * 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) { + 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 void ensureActionButtonIsNonZeroForThisAction(MotionEvent event) { + final int actionButton = event.getActionButton(); + if (actionButton == 0) { + problem("No action button set. Action button should always be non-zero for " + + MotionEvent.actionToString(event.getAction())); + + } + } + + 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 boolean startEvent(InputEvent event, int nestingLevel, 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; + } + + 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; + } + + 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(); + } + if (mViolationMessage.length() == 0) { + mViolationMessage.append(mCurrentEventType).append(": "); + } else { + mViolationMessage.append("\n "); + } + mViolationMessage.append(message); + } + + private 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 addKeyState(int deviceId, int source, int keyCode) { + KeyState state = KeyState.obtain(deviceId, source, keyCode); + state.next = mKeyStateList; + mKeyStateList = state; + } + + private static final class KeyState { + private static Object mRecycledListLock = new Object(); + private static KeyState mRecycledList; + + public KeyState next; + public int deviceId; + public int source; + public int keyCode; + public boolean unhandled; + + private KeyState() { + } + + public static KeyState obtain(int deviceId, int source, int keyCode) { + KeyState state; + synchronized (mRecycledListLock) { + state = mRecycledList; + if (state != null) { + mRecycledList = state.next; + } else { + state = new KeyState(); + } + } + state.deviceId = deviceId; + state.source = source; + state.keyCode = keyCode; + state.unhandled = false; + return state; + } + + public void recycle() { + synchronized (mRecycledListLock) { + next = mRecycledList; + mRecycledList = next; + } + } + } +} |