content: /*
   * Copyright (C) 2008 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.
   * 
   * Modified for aFreeRDP by Martin Fleisz (mfleisz@thinstuff.at)
   */
  
  package me.lancer.airfree.util;
  
  import android.content.Context;
  import android.os.Build;
  import android.os.Handler;
  import android.os.Message;
  import android.util.DisplayMetrics;
  import android.view.MotionEvent;
  import android.view.ViewConfiguration;
  
  public class GestureDetector {
  	
  	/**
       * The listener that is used to notify when gestures occur.
       * If you want to listen for all the different gestures then implement
       * this interface. If you only want to listen for a subset it might
       * be easier to extend {@link SimpleOnGestureListener}.
       */
      public interface OnGestureListener {
  
          /**
           * Notified when a tap occurs with the down {@link MotionEvent}
           * that triggered it. This will be triggered immediately for
           * every down event. All other events should be preceded by this.
           *
           * @param e The down motion event.
           */
          boolean onDown(MotionEvent e);
  
          /**
           * Notified when a tap finishes with the up {@link MotionEvent}
           * that triggered it. This will be triggered immediately for
           * every up event. All other events should be preceded by this.
           *
           * @param e The up motion event.
           */
          boolean onUp(MotionEvent e);
  
          /**
           * The user has performed a down {@link MotionEvent} and not performed
           * a move or up yet. This event is commonly used to provide visual
           * feedback to the user to let them know that their action has been
           * recognized i.e. highlight an element.
           *
           * @param e The down motion event
           */
          void onShowPress(MotionEvent e);
  
          /**
           * Notified when a tap occurs with the up {@link MotionEvent}
           * that triggered it.
           *
           * @param e The up motion event that completed the first tap
           * @return true if the event is consumed, else false
           */
          boolean onSingleTapUp(MotionEvent e);
  
          /**
           * Notified when a scroll occurs with the initial on down {@link MotionEvent} and the
           * current move {@link MotionEvent}. The distance in x and y is also supplied for
           * convenience.
           *
           * @param e1 The first down motion event that started the scrolling.
           * @param e2 The move motion event that triggered the current onScroll.
           * @param distanceX The distance along the X axis that has been scrolled since the last
           *              call to onScroll. This is NOT the distance between {@code e1}
           *              and {@code e2}.
           * @param distanceY The distance along the Y axis that has been scrolled since the last
           *              call to onScroll. This is NOT the distance between {@code e1}
           *              and {@code e2}.
           * @return true if the event is consumed, else false
           */
          boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY);
  
          /**
           * Notified when a long press occurs with the initial on down {@link MotionEvent}
           * that trigged it.
           *
           * @param e The initial on down motion event that started the longpress.
           */
          void onLongPress(MotionEvent e);
  
          /**
           * Notified when a long press ends with the final {@link MotionEvent}.
           *
           * @param e The up motion event that ended the longpress.
           */
          void onLongPressUp(MotionEvent e);
      }
  
      /**
       * The listener that is used to notify when a double-tap or a confirmed
       * single-tap occur.
       */
      public interface OnDoubleTapListener {
          /**
           * Notified when a single-tap occurs.
           * <p>
           * Unlike {@link OnGestureListener#onSingleTapUp(MotionEvent)}, this
           * will only be called after the detector is confident that the user's
           * first tap is not followed by a second tap leading to a double-tap
           * gesture.
           *
           * @param e The down motion event of the single-tap.
           * @return true if the event is consumed, else false
           */
          boolean onSingleTapConfirmed(MotionEvent e);
   
          /**
           * Notified when a double-tap occurs.
           *
           * @param e The down motion event of the first tap of the double-tap.
           * @return true if the event is consumed, else false
           */
          boolean onDoubleTap(MotionEvent e);
  
          /**
           * Notified when an event within a double-tap gesture occurs, including
           * the down, move, and up events.
           *
           * @param e The motion event that occurred during the double-tap gesture.
           * @return true if the event is consumed, else false
           */
          boolean onDoubleTapEvent(MotionEvent e);
      }
  
      /**
       * A convenience class to extend when you only want to listen for a subset
       * of all the gestures. This implements all methods in the
       * {@link OnGestureListener} and {@link OnDoubleTapListener} but does
       * nothing and return {@code false} for all applicable methods.
       */
      public static class SimpleOnGestureListener implements OnGestureListener, OnDoubleTapListener {
          public boolean onSingleTapUp(MotionEvent e) {
              return false;
          }
  
          public void onLongPress(MotionEvent e) {
          }
  
          public void onLongPressUp(MotionEvent e) {
          }
  
          public boolean onScroll(MotionEvent e1, MotionEvent e2,
                  float distanceX, float distanceY) {
              return false;
          }
  
          public void onShowPress(MotionEvent e) {
          }
  
          public boolean onDown(MotionEvent e) {
          	return false;
          }
  
          public boolean onUp(MotionEvent e) {
              return false;
          }
  
          public boolean onDoubleTap(MotionEvent e) {
              return false;
          }
  
          public boolean onDoubleTapEvent(MotionEvent e) {
              return false;
          }
  
          public boolean onSingleTapConfirmed(MotionEvent e) {
              return false;
          }
      }
  
      private int mTouchSlopSquare;
      private int mLargeTouchSlopSquare;
      private int mDoubleTapSlopSquare;
  
      private int mLongpressTimeout = 100;
      private static final int TAP_TIMEOUT = 100;
      private static final int DOUBLE_TAP_TIMEOUT = 200;
  
      // Distance a touch can wander before we think the user is the first touch in a sequence of double tap
  	private static final int LARGE_TOUCH_SLOP = 18;
  
  	// Distance between the first touch and second touch to still be considered a double tap
  	private static final int DOUBLE_TAP_SLOP = 100;	
  
      // constants for Message.what used by GestureHandler below
      private static final int SHOW_PRESS = 1;
      private static final int LONG_PRESS = 2;
      private static final int TAP = 3;
  
      private final Handler mHandler;
      private final OnGestureListener mListener;
      private OnDoubleTapListener mDoubleTapListener;
  
      private boolean mStillDown;
      private boolean mInLongPress;
      private boolean mAlwaysInTapRegion;
      private boolean mAlwaysInBiggerTapRegion;
  
      private MotionEvent mCurrentDownEvent;
      private MotionEvent mPreviousUpEvent;
  
      /**
       * True when the user is still touching for the second tap (down, move, and
       * up events). Can only be true if there is a double tap listener attached.
       */
      private boolean mIsDoubleTapping;
  
      private float mLastMotionY;
      private float mLastMotionX;
  
      private boolean mIsLongpressEnabled;
      
      /**
       * True if we are at a target API level of >= Froyo or the developer can
       * explicitly set it. If true, input events with > 1 pointer will be ignored
       * so we can work side by side with multitouch gesture detectors.
       */
      private boolean mIgnoreMultitouch;
  
      private class GestureHandler extends Handler {
          GestureHandler() {
              super();
          }
  
          GestureHandler(Handler handler) {
              super(handler.getLooper());
          }
  
          @Override
          public void handleMessage(Message msg) {
              switch (msg.what) {
              case SHOW_PRESS:
                  mListener.onShowPress(mCurrentDownEvent);
                  break;
                  
              case LONG_PRESS:
                  dispatchLongPress();
                  break;
                  
              case TAP:
                  // If the user's finger is still down, do not count it as a tap
                  if (mDoubleTapListener != null && !mStillDown) {
                      mDoubleTapListener.onSingleTapConfirmed(mCurrentDownEvent);
                  }
                  break;
  
              default:
                  throw new RuntimeException("Unknown message " + msg); //never
              }
          }
      }
  
      /**
       * Creates a GestureDetector with the supplied listener.
       * You may only use this constructor from a UI thread (this is the usual situation).
       * @see Handler#Handler()
       *
       * @param context the application's context
       * @param listener the listener invoked for all the callbacks, this must
       * not be null.
       *
       * @throws NullPointerException if {@code listener} is null.
       */
      public GestureDetector(Context context, OnGestureListener listener) {
          this(context, listener, null);
      }
  
      /**
       * Creates a GestureDetector with the supplied listener.
       * You may only use this constructor from a UI thread (this is the usual situation).
       * @see Handler#Handler()
       *
       * @param context the application's context
       * @param listener the listener invoked for all the callbacks, this must
       * not be null.
       * @param handler the handler to use     
       *
       * @throws NullPointerException if {@code listener} is null.
       */
      public GestureDetector(Context context, OnGestureListener listener, Handler handler) {
          this(context, listener, handler, context != null &&
                  context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.FROYO);
      }
      
      /**
       * Creates a GestureDetector with the supplied listener.
       * You may only use this constructor from a UI thread (this is the usual situation).
       * @see Handler#Handler()
       *
       * @param context the application's context
       * @param listener the listener invoked for all the callbacks, this must
       * not be null.
       * @param handler the handler to use
       * @param ignoreMultitouch whether events involving more than one pointer should
       * be ignored.
       *
       * @throws NullPointerException if {@code listener} is null.
       */
      public GestureDetector(Context context, OnGestureListener listener, Handler handler,
              boolean ignoreMultitouch) {
          if (handler != null) {
              mHandler = new GestureHandler(handler);
          } else {
              mHandler = new GestureHandler();
          }
          mListener = listener;
          if (listener instanceof OnDoubleTapListener) {
              setOnDoubleTapListener((OnDoubleTapListener) listener);
          }
          init(context, ignoreMultitouch);
      }
  
      private void init(Context context, boolean ignoreMultitouch) {
          if (mListener == null) {
              throw new NullPointerException("OnGestureListener must not be null");
          }
          mIsLongpressEnabled = true;
          mIgnoreMultitouch = ignoreMultitouch;
  
          // Fallback to support pre-donuts releases
          int touchSlop, largeTouchSlop, doubleTapSlop;
          if (context == null) {
              //noinspection deprecation
              touchSlop = ViewConfiguration.getTouchSlop();
              largeTouchSlop = touchSlop + 2;
              doubleTapSlop = DOUBLE_TAP_SLOP;
          } else {
              final DisplayMetrics metrics = context.getResources().getDisplayMetrics();
              final float density = metrics.density;
              final ViewConfiguration configuration = ViewConfiguration.get(context);
              touchSlop = configuration.getScaledTouchSlop();
              largeTouchSlop = (int) (density * LARGE_TOUCH_SLOP + 0.5f);
              doubleTapSlop = configuration.getScaledDoubleTapSlop();
          }
          mTouchSlopSquare = touchSlop * touchSlop;
          mLargeTouchSlopSquare = largeTouchSlop * largeTouchSlop;
          mDoubleTapSlopSquare = doubleTapSlop * doubleTapSlop;
      }
  
      /**
       * Sets the listener which will be called for double-tap and related
       * gestures.
       * 
       * @param onDoubleTapListener the listener invoked for all the callbacks, or
       *        null to stop listening for double-tap gestures.
       */
      public void setOnDoubleTapListener(OnDoubleTapListener onDoubleTapListener) {
          mDoubleTapListener = onDoubleTapListener;
      }
  
      /**
       * Set whether longpress is enabled, if this is enabled when a user
       * presses and holds down you get a longpress event and nothing further.
       * If it's disabled the user can press and hold down and then later
       * moved their finger and you will get scroll events. By default
       * longpress is enabled.
       *
       * @param isLongpressEnabled whether longpress should be enabled.
       */
      public void setIsLongpressEnabled(boolean isLongpressEnabled) {
          mIsLongpressEnabled = isLongpressEnabled;
      }
  
      /**
       * @return true if longpress is enabled, else false.
       */
      public boolean isLongpressEnabled() {
          return mIsLongpressEnabled;
      }
  
      public void setLongPressTimeout(int timeout) {
      	mLongpressTimeout = timeout;
      }
      
      /**
       * Analyzes the given motion event and if applicable triggers the
       * appropriate callbacks on the {@link OnGestureListener} supplied.
       *
       * @param ev The current motion event.
       * @return true if the {@link OnGestureListener} consumed the event,
       *              else false.
       */
      public boolean onTouchEvent(MotionEvent ev) {
          final int action = ev.getAction();
          final float y = ev.getY();
          final float x = ev.getX();
  
          boolean handled = false;
  
          switch (action & MotionEvent.ACTION_MASK) {
          case MotionEvent.ACTION_POINTER_DOWN:
              if (mIgnoreMultitouch) {
                  // Multitouch event - abort.
                  cancel();
              }
              break;
  
          case MotionEvent.ACTION_POINTER_UP:
              // Ending a multitouch gesture and going back to 1 finger
              if (mIgnoreMultitouch && ev.getPointerCount() == 2) {
                  int index = (((action & MotionEvent.ACTION_POINTER_INDEX_MASK)
                          >> MotionEvent.ACTION_POINTER_INDEX_SHIFT) == 0) ? 1 : 0;
                  mLastMotionX = ev.getX(index);
                  mLastMotionY = ev.getY(index);
              }
              break;
  
          case MotionEvent.ACTION_DOWN:
              if (mDoubleTapListener != null) {
                  boolean hadTapMessage = mHandler.hasMessages(TAP);
                  if (hadTapMessage) mHandler.removeMessages(TAP);
                  if ((mCurrentDownEvent != null) && (mPreviousUpEvent != null) && hadTapMessage &&
                          isConsideredDoubleTap(mCurrentDownEvent, mPreviousUpEvent, ev)) {
                      // This is a second tap
                      mIsDoubleTapping = true;
                      // Give a callback with the first tap of the double-tap
                      handled |= mDoubleTapListener.onDoubleTap(mCurrentDownEvent);
                      // Give a callback with down event of the double-tap
                      handled |= mDoubleTapListener.onDoubleTapEvent(ev);
                  } else {
                      // This is a first tap
                      mHandler.sendEmptyMessageDelayed(TAP, DOUBLE_TAP_TIMEOUT);
                  }
              }
  
              mLastMotionX = x;
              mLastMotionY = y;
              if (mCurrentDownEvent != null) {
                  mCurrentDownEvent.recycle();
              }
              mCurrentDownEvent = MotionEvent.obtain(ev);
              mAlwaysInTapRegion = true;
              mAlwaysInBiggerTapRegion = true;
              mStillDown = true;
              mInLongPress = false;
              
              if (mIsLongpressEnabled) {
                  mHandler.removeMessages(LONG_PRESS);
                  mHandler.sendEmptyMessageAtTime(LONG_PRESS, mCurrentDownEvent.getDownTime()
                          + TAP_TIMEOUT + mLongpressTimeout);
              }
              mHandler.sendEmptyMessageAtTime(SHOW_PRESS, mCurrentDownEvent.getDownTime() + TAP_TIMEOUT);
              handled |= mListener.onDown(ev);
              break;
  
          case MotionEvent.ACTION_MOVE:
              if (mIgnoreMultitouch && ev.getPointerCount() > 1) {
                  break;
              }
              final float scrollX = mLastMotionX - x;
              final float scrollY = mLastMotionY - y;
              if (mIsDoubleTapping) {
                  // Give the move events of the double-tap
                  handled |= mDoubleTapListener.onDoubleTapEvent(ev);
              } else if (mAlwaysInTapRegion) {
                  final int deltaX = (int) (x - mCurrentDownEvent.getX());
                  final int deltaY = (int) (y - mCurrentDownEvent.getY());
                  int distance = (deltaX * deltaX) + (deltaY * deltaY);
                  if (distance > mTouchSlopSquare) {
                      mLastMotionX = x;
                      mLastMotionY = y;
                      mAlwaysInTapRegion = false;
                      mHandler.removeMessages(TAP);
                      mHandler.removeMessages(SHOW_PRESS);
                      mHandler.removeMessages(LONG_PRESS);
                  }
                  if (distance > mLargeTouchSlopSquare) {
                      mAlwaysInBiggerTapRegion = false;
                  }
                  handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
              } else if ((Math.abs(scrollX) >= 1) || (Math.abs(scrollY) >= 1)) {
                  handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
                  mLastMotionX = x;
                  mLastMotionY = y;
              }
              break;
  
          case MotionEvent.ACTION_UP:
              mStillDown = false;
              MotionEvent currentUpEvent = MotionEvent.obtain(ev);
              if (mIsDoubleTapping) {
                  // Finally, give the up event of the double-tap
                  handled |= mDoubleTapListener.onDoubleTapEvent(ev);
              } else if (mInLongPress) {
                  mHandler.removeMessages(TAP);
                  mListener.onLongPressUp(ev);
                  mInLongPress = false;
              } else if (mAlwaysInTapRegion) {
                  handled = mListener.onSingleTapUp(mCurrentDownEvent);
              } else {
                  // A fling must travel the minimum tap distance
              }
              if (mPreviousUpEvent != null) {
                  mPreviousUpEvent.recycle();
              }
              // Hold the event we obtained above - listeners may have changed the original.
              mPreviousUpEvent = currentUpEvent;
              mIsDoubleTapping = false;
              mHandler.removeMessages(SHOW_PRESS);
              mHandler.removeMessages(LONG_PRESS);
              handled |= mListener.onUp(ev);
              break;
          case MotionEvent.ACTION_CANCEL:
              cancel();
              break;
          }
          return handled;
      }
  
      private void cancel() {
          mHandler.removeMessages(SHOW_PRESS);
          mHandler.removeMessages(LONG_PRESS);
          mHandler.removeMessages(TAP);
          mAlwaysInTapRegion = false; // ensures that we won't receive an OnSingleTap notification when a 2-Finger tap is performed
          mIsDoubleTapping = false;
          mStillDown = false;
          if (mInLongPress) {
              mInLongPress = false;
          }
      }
  
      private boolean isConsideredDoubleTap(MotionEvent firstDown, MotionEvent firstUp,
              MotionEvent secondDown) {
          if (!mAlwaysInBiggerTapRegion) {
              return false;
          }
  
          if (secondDown.getEventTime() - firstUp.getEventTime() > DOUBLE_TAP_TIMEOUT) {
              return false;
          }
  
          int deltaX = (int) firstDown.getX() - (int) secondDown.getX();
          int deltaY = (int) firstDown.getY() - (int) secondDown.getY();
          return (deltaX * deltaX + deltaY * deltaY < mDoubleTapSlopSquare);
      }
  
      private void dispatchLongPress() {
          mHandler.removeMessages(TAP);
          mInLongPress = true;
          mListener.onLongPress(mCurrentDownEvent);
      }
  }
  