mobile/android/base/gfx/TouchEventHandler.java

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/mobile/android/base/gfx/TouchEventHandler.java	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,308 @@
     1.4 +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
     1.5 + * This Source Code Form is subject to the terms of the Mozilla Public
     1.6 + * License, v. 2.0. If a copy of the MPL was not distributed with this file,
     1.7 + * You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.8 +
     1.9 +package org.mozilla.gecko.gfx;
    1.10 +
    1.11 +import org.mozilla.gecko.Tab;
    1.12 +import org.mozilla.gecko.Tabs;
    1.13 +
    1.14 +import android.content.Context;
    1.15 +import android.os.SystemClock;
    1.16 +import android.util.Log;
    1.17 +import android.view.GestureDetector;
    1.18 +import android.view.MotionEvent;
    1.19 +import android.view.View;
    1.20 +
    1.21 +import java.util.LinkedList;
    1.22 +import java.util.Queue;
    1.23 +
    1.24 +/**
    1.25 + * This class handles incoming touch events from the user and sends them to
    1.26 + * listeners in Gecko and/or performs the "default action" (asynchronous pan/zoom
    1.27 + * behaviour. EVERYTHING IN THIS CLASS MUST RUN ON THE UI THREAD.
    1.28 + *
    1.29 + * In the following code/comments, a "block" of events refers to a contiguous
    1.30 + * sequence of events that starts with a DOWN or POINTER_DOWN and goes up to
    1.31 + * but not including the next DOWN or POINTER_DOWN event.
    1.32 + *
    1.33 + * "Dispatching" an event refers to performing the default actions for the event,
    1.34 + * which at our level of abstraction just means sending it off to the gesture
    1.35 + * detectors and the pan/zoom controller.
    1.36 + *
    1.37 + * If an event is "default-prevented" that means one or more listeners in Gecko
    1.38 + * has called preventDefault() on the event, which means that the default action
    1.39 + * for that event should not occur. Usually we care about a "block" of events being
    1.40 + * default-prevented, which means that the DOWN/POINTER_DOWN event that started
    1.41 + * the block, or the first MOVE event following that, were prevent-defaulted.
    1.42 + *
    1.43 + * A "default-prevented notification" is when we here in Java-land receive a notification
    1.44 + * from gecko as to whether or not a block of events was default-prevented. This happens
    1.45 + * at some point after the first or second event in the block is processed in Gecko.
    1.46 + * This code assumes we get EXACTLY ONE default-prevented notification for each block
    1.47 + * of events.
    1.48 + *
    1.49 + * Note that even if all events are default-prevented, we still send specific types
    1.50 + * of notifications to the pan/zoom controller. The notifications are needed
    1.51 + * to respond to user actions a timely manner regardless of default-prevention,
    1.52 + * and fix issues like bug 749384.
    1.53 + */
    1.54 +final class TouchEventHandler implements Tabs.OnTabsChangedListener {
    1.55 +    private static final String LOGTAG = "GeckoTouchEventHandler";
    1.56 +
    1.57 +    // The time limit for listeners to respond with preventDefault on touchevents
    1.58 +    // before we begin panning the page
    1.59 +    private final int EVENT_LISTENER_TIMEOUT = 200;
    1.60 +
    1.61 +    private final View mView;
    1.62 +    private final GestureDetector mGestureDetector;
    1.63 +    private final SimpleScaleGestureDetector mScaleGestureDetector;
    1.64 +    private final JavaPanZoomController mPanZoomController;
    1.65 +
    1.66 +    // the queue of events that we are holding on to while waiting for a preventDefault
    1.67 +    // notification
    1.68 +    private final Queue<MotionEvent> mEventQueue;
    1.69 +    private final ListenerTimeoutProcessor mListenerTimeoutProcessor;
    1.70 +
    1.71 +    // whether or not we should wait for touch listeners to respond (this state is
    1.72 +    // per-tab and is updated when we switch tabs).
    1.73 +    private boolean mWaitForTouchListeners;
    1.74 +
    1.75 +    // true if we should hold incoming events in our queue. this is re-set for every
    1.76 +    // block of events, this is cleared once we find out if the block has been
    1.77 +    // default-prevented or not (or we time out waiting for that).
    1.78 +    private boolean mHoldInQueue;
    1.79 +
    1.80 +    // false if the current event block has been default-prevented. In this case,
    1.81 +    // we still pass the event to both Gecko and the pan/zoom controller, but the
    1.82 +    // latter will not use it to scroll content. It may still use the events for
    1.83 +    // other things, such as making the dynamic toolbar visible.
    1.84 +    private boolean mAllowDefaultAction;
    1.85 +
    1.86 +    // this next variable requires some explanation. strap yourself in.
    1.87 +    //
    1.88 +    // for each block of events, we do two things: (1) send the events to gecko and expect
    1.89 +    // exactly one default-prevented notification in return, and (2) kick off a delayed
    1.90 +    // ListenerTimeoutProcessor that triggers in case we don't hear from the listener in
    1.91 +    // a timely fashion.
    1.92 +    // since events are constantly coming in, we need to be able to handle more than one
    1.93 +    // block of events in the queue.
    1.94 +    //
    1.95 +    // this means that there are ordering restrictions on these that we can take advantage of,
    1.96 +    // and need to abide by. blocks of events in the queue will always be in the order that
    1.97 +    // the user generated them. default-prevented notifications we get from gecko will be in
    1.98 +    // the same order as the blocks of events in the queue. the ListenerTimeoutProcessors that
    1.99 +    // have been posted will also fire in the same order as the blocks of events in the queue.
   1.100 +    // HOWEVER, we may get multiple default-prevented notifications interleaved with multiple
   1.101 +    // ListenerTimeoutProcessor firings, and that interleaving is not predictable.
   1.102 +    //
   1.103 +    // therefore, we need to make sure that for each block of events, we process the queued
   1.104 +    // events exactly once, either when we get the default-prevented notification, or when the
   1.105 +    // timeout expires (whichever happens first). there is no way to associate the
   1.106 +    // default-prevented notification with a particular block of events other than via ordering,
   1.107 +    //
   1.108 +    // so what we do to accomplish this is to track a "processing balance", which is the number
   1.109 +    // of default-prevented notifications that we have received, minus the number of ListenerTimeoutProcessors
   1.110 +    // that have fired. (think "balance" as in teeter-totter balance). this value is:
   1.111 +    // - zero when we are in a state where the next default-prevented notification we expect
   1.112 +    //   to receive and the next ListenerTimeoutProcessor we expect to fire both correspond to
   1.113 +    //   the next block of events in the queue.
   1.114 +    // - positive when we are in a state where we have received more default-prevented notifications
   1.115 +    //   than ListenerTimeoutProcessors. This means that the next default-prevented notification
   1.116 +    //   does correspond to the block at the head of the queue, but the next n ListenerTimeoutProcessors
   1.117 +    //   need to be ignored as they are for blocks we have already processed. (n is the absolute value
   1.118 +    //   of the balance.)
   1.119 +    // - negative when we are in a state where we have received more ListenerTimeoutProcessors than
   1.120 +    //   default-prevented notifications. This means that the next ListenerTimeoutProcessor that
   1.121 +    //   we receive does correspond to the block at the head of the queue, but the next n
   1.122 +    //   default-prevented notifications need to be ignored as they are for blocks we have already
   1.123 +    //   processed. (n is the absolute value of the balance.)
   1.124 +    private int mProcessingBalance;
   1.125 +
   1.126 +    TouchEventHandler(Context context, View view, JavaPanZoomController panZoomController) {
   1.127 +        mView = view;
   1.128 +
   1.129 +        mEventQueue = new LinkedList<MotionEvent>();
   1.130 +        mPanZoomController = panZoomController;
   1.131 +        mGestureDetector = new GestureDetector(context, mPanZoomController);
   1.132 +        mScaleGestureDetector = new SimpleScaleGestureDetector(mPanZoomController);
   1.133 +        mListenerTimeoutProcessor = new ListenerTimeoutProcessor();
   1.134 +        mAllowDefaultAction = true;
   1.135 +
   1.136 +        mGestureDetector.setOnDoubleTapListener(mPanZoomController);
   1.137 +
   1.138 +        Tabs.registerOnTabsChangedListener(this);
   1.139 +    }
   1.140 +
   1.141 +    public void destroy() {
   1.142 +        Tabs.unregisterOnTabsChangedListener(this);
   1.143 +    }
   1.144 +
   1.145 +    /* This function MUST be called on the UI thread */
   1.146 +    public boolean handleEvent(MotionEvent event) {
   1.147 +        if (isDownEvent(event)) {
   1.148 +            // this is the start of a new block of events! whee!
   1.149 +            mHoldInQueue = mWaitForTouchListeners;
   1.150 +
   1.151 +            // Set mAllowDefaultAction to true so that in the event we dispatch events, the
   1.152 +            // PanZoomController doesn't treat them as if they've been prevent-defaulted
   1.153 +            // when they haven't.
   1.154 +            mAllowDefaultAction = true;
   1.155 +            if (mHoldInQueue) {
   1.156 +                // if the new block we are starting is the current block (i.e. there are no
   1.157 +                // other blocks waiting in the queue, then we should let the pan/zoom controller
   1.158 +                // know we are waiting for the touch listeners to run
   1.159 +                if (mEventQueue.isEmpty()) {
   1.160 +                    mPanZoomController.startingNewEventBlock(event, true);
   1.161 +                }
   1.162 +            } else {
   1.163 +                // we're not going to be holding this block of events in the queue, but we need
   1.164 +                // a marker of some sort so that the processEventBlock loop deals with the blocks
   1.165 +                // in the right order as notifications come in. we use a single null event in
   1.166 +                // the queue as a placeholder for a block of events that has already been dispatched.
   1.167 +                mEventQueue.add(null);
   1.168 +                mPanZoomController.startingNewEventBlock(event, false);
   1.169 +            }
   1.170 +
   1.171 +            // set the timeout so that we dispatch these events and update mProcessingBalance
   1.172 +            // if we don't get a default-prevented notification
   1.173 +            mView.postDelayed(mListenerTimeoutProcessor, EVENT_LISTENER_TIMEOUT);
   1.174 +        }
   1.175 +
   1.176 +        // if we need to hold the events, add it to the queue, otherwise dispatch
   1.177 +        // it directly.
   1.178 +        if (mHoldInQueue) {
   1.179 +            mEventQueue.add(MotionEvent.obtain(event));
   1.180 +        } else {
   1.181 +            dispatchEvent(event, mAllowDefaultAction);
   1.182 +        }
   1.183 +
   1.184 +        return false;
   1.185 +    }
   1.186 +
   1.187 +    /**
   1.188 +     * This function is how gecko sends us a default-prevented notification. It is called
   1.189 +     * once gecko knows definitively whether the block of events has had preventDefault
   1.190 +     * called on it (either on the initial down event that starts the block, or on
   1.191 +     * the first event following that down event).
   1.192 +     *
   1.193 +     * This function MUST be called on the UI thread.
   1.194 +     */
   1.195 +    public void handleEventListenerAction(boolean allowDefaultAction) {
   1.196 +        if (mProcessingBalance > 0) {
   1.197 +            // this event listener that triggered this took too long, and the corresponding
   1.198 +            // ListenerTimeoutProcessor runnable already ran for the event in question. the
   1.199 +            // block of events this is for has already been processed, so we don't need to
   1.200 +            // do anything here.
   1.201 +        } else {
   1.202 +            processEventBlock(allowDefaultAction);
   1.203 +        }
   1.204 +        mProcessingBalance--;
   1.205 +    }
   1.206 +
   1.207 +    /* This function MUST be called on the UI thread. */
   1.208 +    public void setWaitForTouchListeners(boolean aValue) {
   1.209 +        mWaitForTouchListeners = aValue;
   1.210 +    }
   1.211 +
   1.212 +    private boolean isDownEvent(MotionEvent event) {
   1.213 +        int action = (event.getAction() & MotionEvent.ACTION_MASK);
   1.214 +        return (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_POINTER_DOWN);
   1.215 +    }
   1.216 +
   1.217 +    private boolean touchFinished(MotionEvent event) {
   1.218 +        int action = (event.getAction() & MotionEvent.ACTION_MASK);
   1.219 +        return (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL);
   1.220 +    }
   1.221 +
   1.222 +    /**
   1.223 +     * Dispatch the event to the gesture detectors and the pan/zoom controller.
   1.224 +     */
   1.225 +    private void dispatchEvent(MotionEvent event, boolean allowDefaultAction) {
   1.226 +        if (allowDefaultAction) {
   1.227 +            if (mGestureDetector.onTouchEvent(event)) {
   1.228 +                return;
   1.229 +            }
   1.230 +            mScaleGestureDetector.onTouchEvent(event);
   1.231 +            if (mScaleGestureDetector.isInProgress()) {
   1.232 +                return;
   1.233 +            }
   1.234 +        }
   1.235 +        mPanZoomController.handleEvent(event, !allowDefaultAction);
   1.236 +    }
   1.237 +
   1.238 +    /**
   1.239 +     * Process the block of events at the head of the queue now that we know
   1.240 +     * whether it has been default-prevented or not.
   1.241 +     */
   1.242 +    private void processEventBlock(boolean allowDefaultAction) {
   1.243 +        if (mEventQueue.isEmpty()) {
   1.244 +            Log.e(LOGTAG, "Unexpected empty event queue in processEventBlock!", new Exception());
   1.245 +            return;
   1.246 +        }
   1.247 +
   1.248 +        // the odd loop condition is because the first event in the queue will
   1.249 +        // always be a DOWN or POINTER_DOWN event, and we want to process all
   1.250 +        // the events in the queue starting at that one, up to but not including
   1.251 +        // the next DOWN or POINTER_DOWN event.
   1.252 +
   1.253 +        MotionEvent event = mEventQueue.poll();
   1.254 +        while (true) {
   1.255 +            // event being null here is valid and represents a block of events
   1.256 +            // that has already been dispatched.
   1.257 +
   1.258 +            if (event != null) {
   1.259 +                dispatchEvent(event, allowDefaultAction);
   1.260 +                event.recycle();
   1.261 +                event = null;
   1.262 +            }
   1.263 +            if (mEventQueue.isEmpty()) {
   1.264 +                // we have processed the backlog of events, and are all caught up.
   1.265 +                // now we can set clear the hold flag and set the dispatch flag so
   1.266 +                // that the handleEvent() function can do the right thing for all
   1.267 +                // remaining events in this block (which is still ongoing) without
   1.268 +                // having to put them in the queue.
   1.269 +                mHoldInQueue = false;
   1.270 +                mAllowDefaultAction = allowDefaultAction;
   1.271 +                break;
   1.272 +            }
   1.273 +            event = mEventQueue.peek();
   1.274 +            if (event == null || isDownEvent(event)) {
   1.275 +                // we have finished processing the block we were interested in.
   1.276 +                // now we wait for the next call to processEventBlock
   1.277 +                if (event != null) {
   1.278 +                    mPanZoomController.startingNewEventBlock(event, true);
   1.279 +                }
   1.280 +                break;
   1.281 +            }
   1.282 +            // pop the event we peeked above, as it is still part of the block and
   1.283 +            // we want to keep processing
   1.284 +            mEventQueue.remove();
   1.285 +        }
   1.286 +    }
   1.287 +
   1.288 +    private class ListenerTimeoutProcessor implements Runnable {
   1.289 +        /* This MUST be run on the UI thread */
   1.290 +        @Override
   1.291 +        public void run() {
   1.292 +            if (mProcessingBalance < 0) {
   1.293 +                // gecko already responded with default-prevented notification, and so
   1.294 +                // the block of events this ListenerTimeoutProcessor corresponds to have
   1.295 +                // already been removed from the queue.
   1.296 +            } else {
   1.297 +                processEventBlock(true);
   1.298 +            }
   1.299 +            mProcessingBalance++;
   1.300 +        }
   1.301 +    }
   1.302 +
   1.303 +    // Tabs.OnTabsChangedListener implementation
   1.304 +
   1.305 +    @Override
   1.306 +    public void onTabChanged(Tab tab, Tabs.TabEvents msg, Object data) {
   1.307 +        if ((Tabs.getInstance().isSelectedTab(tab) && msg == Tabs.TabEvents.STOP) || msg == Tabs.TabEvents.SELECTED) {
   1.308 +            mWaitForTouchListeners = tab.getHasTouchListeners();
   1.309 +        }
   1.310 +    }
   1.311 +}

mercurial