// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.content.browser;
import android.content.Context;
import android.os.Handler;
import android.os.Message;
import android.view.MotionEvent;
import android.view.ViewConfiguration;
import java.util.Iterator;
/**
* This class controls long press timers and is owned by a ContentViewGestureHandler.
*
* For instance, we may receive a DOWN then UP, so we may need to cancel the
* timer before the UP completes its roundtrip from WebKit.
*/
class LongPressDetector {
private MotionEvent mCurrentDownEvent;
private final LongPressDelegate mLongPressDelegate;
private final Handler mLongPressHandler;
private final int mTouchSlopSquare;
private boolean mInLongPress;
// The following are used when touch events are offered to native, and not for
// anything relating to the GestureDetector.
// True iff a touch_move has exceeded the touch slop distance.
private boolean mMoveConfirmed;
// Coordinates of the start of a touch event (i.e. the touch_down).
private int mTouchInitialX;
private int mTouchInitialY;
private static final int LONG_PRESS = 2;
private static final int LONGPRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout();
private static final int TAP_TIMEOUT = ViewConfiguration.getTapTimeout();
LongPressDetector(Context context, LongPressDelegate delegate) {
mLongPressDelegate = delegate;
mLongPressHandler = new LongPressHandler();
final ViewConfiguration configuration = ViewConfiguration.get(context);
int touchSlop = configuration.getScaledTouchSlop();
mTouchSlopSquare = touchSlop * touchSlop;
}
private class LongPressHandler extends Handler {
LongPressHandler() {
super();
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case LONG_PRESS:
dispatchLongPress();
break;
default:
throw new RuntimeException("Unknown message " + msg); //never
}
}
}
/**
* This is an interface to execute the LongPress when it receives the onLongPress message.
*/
interface LongPressDelegate {
/**
* @param event The event will be recycled after this call has returned.
*/
public void onLongPress(MotionEvent event);
}
/**
* Initiates a LONG_PRESS gesture timer if needed.
*/
void startLongPressTimerIfNeeded(MotionEvent ev) {
if (ev.getAction() != MotionEvent.ACTION_DOWN) return;
// If there is a current down, we do not expect another down event before
// receiving an up event
if (mCurrentDownEvent != null) return;
mCurrentDownEvent = MotionEvent.obtain(ev);
mLongPressHandler.sendEmptyMessageAtTime(LONG_PRESS, mCurrentDownEvent.getDownTime()
+ TAP_TIMEOUT + LONGPRESS_TIMEOUT);
mInLongPress = false;
}
// Cancel LONG_PRESS timers.
void cancelLongPressIfNeeded(MotionEvent ev) {
if (!hasPendingMessage() ||
mCurrentDownEvent == null || ev.getDownTime() != mCurrentDownEvent.getDownTime()) {
return;
}
final int action = ev.getAction();
final float y = ev.getY();
final float x = ev.getX();
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_MOVE:
final int deltaX = (int) (x - mCurrentDownEvent.getX());
final int deltaY = (int) (y - mCurrentDownEvent.getY());
int distance = (deltaX * deltaX) + (deltaY * deltaY);
if (distance > mTouchSlopSquare) {
cancelLongPress();
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
if (mCurrentDownEvent.getDownTime() + TAP_TIMEOUT + LONGPRESS_TIMEOUT >
ev.getEventTime()) {
cancelLongPress();
}
break;
default:
break;
}
}
// Given a stream of pending events, cancel the LONG_PRESS timer if appropriate.
void cancelLongPressIfNeeded(Iterator<MotionEvent> pendingEvents) {
if (mCurrentDownEvent == null)
return;
long currentDownTime = mCurrentDownEvent.getDownTime();
while (pendingEvents.hasNext()) {
MotionEvent pending = pendingEvents.next();
if (pending.getDownTime() != currentDownTime) {
break;
}
cancelLongPressIfNeeded(pending);
}
}
void cancelLongPress() {
mInLongPress = false;
if (hasPendingMessage()) {
mLongPressHandler.removeMessages(LONG_PRESS);
mCurrentDownEvent.recycle();
mCurrentDownEvent = null;
}
}
// Used this to check if a onSingleTapUp is part of a long press event.
boolean isInLongPress() {
return mInLongPress;
}
private void dispatchLongPress() {
mInLongPress = true;
mLongPressDelegate.onLongPress(mCurrentDownEvent);
mCurrentDownEvent.recycle();
mCurrentDownEvent = null;
}
boolean hasPendingMessage() {
return mCurrentDownEvent != null;
}
void onOfferTouchEventToJavaScript(MotionEvent event) {
if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
mMoveConfirmed = false;
mTouchInitialX = Math.round(event.getX());
mTouchInitialY = Math.round(event.getY());
}
}
boolean confirmOfferMoveEventToJavaScript(MotionEvent event) {
if (!mMoveConfirmed) {
int deltaX = Math.round(event.getX()) - mTouchInitialX;
int deltaY = Math.round(event.getY()) - mTouchInitialY;
if (deltaX * deltaX + deltaY * deltaY >= mTouchSlopSquare) {
mMoveConfirmed = true;
}
}
return mMoveConfirmed;
}
/**
* This is for testing only.
* Sends a LongPress gesture. This should always be called after a down event.
*/
void sendLongPressGestureForTest() {
if (mCurrentDownEvent == null) return;
dispatchLongPress();
}
}