@Override
  public boolean onTouchEvent(MotionEvent event) {
    // TODO(abarth): This version check might not be effective in some
    // versions of Android that statically compile code and will be upset
    // at the lack of |requestUnbufferedDispatch|. Instead, we should factor
    // version-dependent code into separate classes for each supported
    // version and dispatch dynamically.
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
      requestUnbufferedDispatch(event);
    }

    ArrayList<Pointer> pointers = new ArrayList<Pointer>();

    // TODO(abarth): Rather than unpacking these events here, we should
    // probably send them in one packet to the engine.
    int maskedAction = event.getActionMasked();
    // ACTION_UP, ACTION_POINTER_UP, ACTION_DOWN, and ACTION_POINTER_DOWN
    // only apply to a single pointer, other events apply to all pointers.
    if (maskedAction == MotionEvent.ACTION_UP
        || maskedAction == MotionEvent.ACTION_POINTER_UP
        || maskedAction == MotionEvent.ACTION_DOWN
        || maskedAction == MotionEvent.ACTION_POINTER_DOWN) {
      addPointerForIndex(event, event.getActionIndex(), pointers);
    } else {
      // ACTION_MOVE may not actually mean all pointers have moved
      // but it's the responsibility of a later part of the system to
      // ignore 0-deltas if desired.
      for (int p = 0; p < event.getPointerCount(); p++) {
        addPointerForIndex(event, p, pointers);
      }
    }

    PointerPacket packet = new PointerPacket();
    packet.pointers = pointers.toArray(new Pointer[0]);
    mSkyEngine.onPointerPacket(packet);

    return true;
  }