content: /*
   * 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.annotation.UnsupportedAppUsage;
  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.
       */
      @UnsupportedAppUsage
      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.
       */
      @UnsupportedAppUsage
      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.
       */
      @UnsupportedAppUsage
      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.
       */
      @UnsupportedAppUsage
      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;
              }
          }
      }
  }
  