blob: 7e8ec0469e565854e74c8097fcf21e041fb69bd9 [file] [log] [blame]
/*
* 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;
}
}
}
}