mobile/android/base/gfx/TouchEventHandler.java

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

     1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
     2  * This Source Code Form is subject to the terms of the Mozilla Public
     3  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
     4  * You can obtain one at http://mozilla.org/MPL/2.0/. */
     6 package org.mozilla.gecko.gfx;
     8 import org.mozilla.gecko.Tab;
     9 import org.mozilla.gecko.Tabs;
    11 import android.content.Context;
    12 import android.os.SystemClock;
    13 import android.util.Log;
    14 import android.view.GestureDetector;
    15 import android.view.MotionEvent;
    16 import android.view.View;
    18 import java.util.LinkedList;
    19 import java.util.Queue;
    21 /**
    22  * This class handles incoming touch events from the user and sends them to
    23  * listeners in Gecko and/or performs the "default action" (asynchronous pan/zoom
    24  * behaviour. EVERYTHING IN THIS CLASS MUST RUN ON THE UI THREAD.
    25  *
    26  * In the following code/comments, a "block" of events refers to a contiguous
    27  * sequence of events that starts with a DOWN or POINTER_DOWN and goes up to
    28  * but not including the next DOWN or POINTER_DOWN event.
    29  *
    30  * "Dispatching" an event refers to performing the default actions for the event,
    31  * which at our level of abstraction just means sending it off to the gesture
    32  * detectors and the pan/zoom controller.
    33  *
    34  * If an event is "default-prevented" that means one or more listeners in Gecko
    35  * has called preventDefault() on the event, which means that the default action
    36  * for that event should not occur. Usually we care about a "block" of events being
    37  * default-prevented, which means that the DOWN/POINTER_DOWN event that started
    38  * the block, or the first MOVE event following that, were prevent-defaulted.
    39  *
    40  * A "default-prevented notification" is when we here in Java-land receive a notification
    41  * from gecko as to whether or not a block of events was default-prevented. This happens
    42  * at some point after the first or second event in the block is processed in Gecko.
    43  * This code assumes we get EXACTLY ONE default-prevented notification for each block
    44  * of events.
    45  *
    46  * Note that even if all events are default-prevented, we still send specific types
    47  * of notifications to the pan/zoom controller. The notifications are needed
    48  * to respond to user actions a timely manner regardless of default-prevention,
    49  * and fix issues like bug 749384.
    50  */
    51 final class TouchEventHandler implements Tabs.OnTabsChangedListener {
    52     private static final String LOGTAG = "GeckoTouchEventHandler";
    54     // The time limit for listeners to respond with preventDefault on touchevents
    55     // before we begin panning the page
    56     private final int EVENT_LISTENER_TIMEOUT = 200;
    58     private final View mView;
    59     private final GestureDetector mGestureDetector;
    60     private final SimpleScaleGestureDetector mScaleGestureDetector;
    61     private final JavaPanZoomController mPanZoomController;
    63     // the queue of events that we are holding on to while waiting for a preventDefault
    64     // notification
    65     private final Queue<MotionEvent> mEventQueue;
    66     private final ListenerTimeoutProcessor mListenerTimeoutProcessor;
    68     // whether or not we should wait for touch listeners to respond (this state is
    69     // per-tab and is updated when we switch tabs).
    70     private boolean mWaitForTouchListeners;
    72     // true if we should hold incoming events in our queue. this is re-set for every
    73     // block of events, this is cleared once we find out if the block has been
    74     // default-prevented or not (or we time out waiting for that).
    75     private boolean mHoldInQueue;
    77     // false if the current event block has been default-prevented. In this case,
    78     // we still pass the event to both Gecko and the pan/zoom controller, but the
    79     // latter will not use it to scroll content. It may still use the events for
    80     // other things, such as making the dynamic toolbar visible.
    81     private boolean mAllowDefaultAction;
    83     // this next variable requires some explanation. strap yourself in.
    84     //
    85     // for each block of events, we do two things: (1) send the events to gecko and expect
    86     // exactly one default-prevented notification in return, and (2) kick off a delayed
    87     // ListenerTimeoutProcessor that triggers in case we don't hear from the listener in
    88     // a timely fashion.
    89     // since events are constantly coming in, we need to be able to handle more than one
    90     // block of events in the queue.
    91     //
    92     // this means that there are ordering restrictions on these that we can take advantage of,
    93     // and need to abide by. blocks of events in the queue will always be in the order that
    94     // the user generated them. default-prevented notifications we get from gecko will be in
    95     // the same order as the blocks of events in the queue. the ListenerTimeoutProcessors that
    96     // have been posted will also fire in the same order as the blocks of events in the queue.
    97     // HOWEVER, we may get multiple default-prevented notifications interleaved with multiple
    98     // ListenerTimeoutProcessor firings, and that interleaving is not predictable.
    99     //
   100     // therefore, we need to make sure that for each block of events, we process the queued
   101     // events exactly once, either when we get the default-prevented notification, or when the
   102     // timeout expires (whichever happens first). there is no way to associate the
   103     // default-prevented notification with a particular block of events other than via ordering,
   104     //
   105     // so what we do to accomplish this is to track a "processing balance", which is the number
   106     // of default-prevented notifications that we have received, minus the number of ListenerTimeoutProcessors
   107     // that have fired. (think "balance" as in teeter-totter balance). this value is:
   108     // - zero when we are in a state where the next default-prevented notification we expect
   109     //   to receive and the next ListenerTimeoutProcessor we expect to fire both correspond to
   110     //   the next block of events in the queue.
   111     // - positive when we are in a state where we have received more default-prevented notifications
   112     //   than ListenerTimeoutProcessors. This means that the next default-prevented notification
   113     //   does correspond to the block at the head of the queue, but the next n ListenerTimeoutProcessors
   114     //   need to be ignored as they are for blocks we have already processed. (n is the absolute value
   115     //   of the balance.)
   116     // - negative when we are in a state where we have received more ListenerTimeoutProcessors than
   117     //   default-prevented notifications. This means that the next ListenerTimeoutProcessor that
   118     //   we receive does correspond to the block at the head of the queue, but the next n
   119     //   default-prevented notifications need to be ignored as they are for blocks we have already
   120     //   processed. (n is the absolute value of the balance.)
   121     private int mProcessingBalance;
   123     TouchEventHandler(Context context, View view, JavaPanZoomController panZoomController) {
   124         mView = view;
   126         mEventQueue = new LinkedList<MotionEvent>();
   127         mPanZoomController = panZoomController;
   128         mGestureDetector = new GestureDetector(context, mPanZoomController);
   129         mScaleGestureDetector = new SimpleScaleGestureDetector(mPanZoomController);
   130         mListenerTimeoutProcessor = new ListenerTimeoutProcessor();
   131         mAllowDefaultAction = true;
   133         mGestureDetector.setOnDoubleTapListener(mPanZoomController);
   135         Tabs.registerOnTabsChangedListener(this);
   136     }
   138     public void destroy() {
   139         Tabs.unregisterOnTabsChangedListener(this);
   140     }
   142     /* This function MUST be called on the UI thread */
   143     public boolean handleEvent(MotionEvent event) {
   144         if (isDownEvent(event)) {
   145             // this is the start of a new block of events! whee!
   146             mHoldInQueue = mWaitForTouchListeners;
   148             // Set mAllowDefaultAction to true so that in the event we dispatch events, the
   149             // PanZoomController doesn't treat them as if they've been prevent-defaulted
   150             // when they haven't.
   151             mAllowDefaultAction = true;
   152             if (mHoldInQueue) {
   153                 // if the new block we are starting is the current block (i.e. there are no
   154                 // other blocks waiting in the queue, then we should let the pan/zoom controller
   155                 // know we are waiting for the touch listeners to run
   156                 if (mEventQueue.isEmpty()) {
   157                     mPanZoomController.startingNewEventBlock(event, true);
   158                 }
   159             } else {
   160                 // we're not going to be holding this block of events in the queue, but we need
   161                 // a marker of some sort so that the processEventBlock loop deals with the blocks
   162                 // in the right order as notifications come in. we use a single null event in
   163                 // the queue as a placeholder for a block of events that has already been dispatched.
   164                 mEventQueue.add(null);
   165                 mPanZoomController.startingNewEventBlock(event, false);
   166             }
   168             // set the timeout so that we dispatch these events and update mProcessingBalance
   169             // if we don't get a default-prevented notification
   170             mView.postDelayed(mListenerTimeoutProcessor, EVENT_LISTENER_TIMEOUT);
   171         }
   173         // if we need to hold the events, add it to the queue, otherwise dispatch
   174         // it directly.
   175         if (mHoldInQueue) {
   176             mEventQueue.add(MotionEvent.obtain(event));
   177         } else {
   178             dispatchEvent(event, mAllowDefaultAction);
   179         }
   181         return false;
   182     }
   184     /**
   185      * This function is how gecko sends us a default-prevented notification. It is called
   186      * once gecko knows definitively whether the block of events has had preventDefault
   187      * called on it (either on the initial down event that starts the block, or on
   188      * the first event following that down event).
   189      *
   190      * This function MUST be called on the UI thread.
   191      */
   192     public void handleEventListenerAction(boolean allowDefaultAction) {
   193         if (mProcessingBalance > 0) {
   194             // this event listener that triggered this took too long, and the corresponding
   195             // ListenerTimeoutProcessor runnable already ran for the event in question. the
   196             // block of events this is for has already been processed, so we don't need to
   197             // do anything here.
   198         } else {
   199             processEventBlock(allowDefaultAction);
   200         }
   201         mProcessingBalance--;
   202     }
   204     /* This function MUST be called on the UI thread. */
   205     public void setWaitForTouchListeners(boolean aValue) {
   206         mWaitForTouchListeners = aValue;
   207     }
   209     private boolean isDownEvent(MotionEvent event) {
   210         int action = (event.getAction() & MotionEvent.ACTION_MASK);
   211         return (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_POINTER_DOWN);
   212     }
   214     private boolean touchFinished(MotionEvent event) {
   215         int action = (event.getAction() & MotionEvent.ACTION_MASK);
   216         return (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL);
   217     }
   219     /**
   220      * Dispatch the event to the gesture detectors and the pan/zoom controller.
   221      */
   222     private void dispatchEvent(MotionEvent event, boolean allowDefaultAction) {
   223         if (allowDefaultAction) {
   224             if (mGestureDetector.onTouchEvent(event)) {
   225                 return;
   226             }
   227             mScaleGestureDetector.onTouchEvent(event);
   228             if (mScaleGestureDetector.isInProgress()) {
   229                 return;
   230             }
   231         }
   232         mPanZoomController.handleEvent(event, !allowDefaultAction);
   233     }
   235     /**
   236      * Process the block of events at the head of the queue now that we know
   237      * whether it has been default-prevented or not.
   238      */
   239     private void processEventBlock(boolean allowDefaultAction) {
   240         if (mEventQueue.isEmpty()) {
   241             Log.e(LOGTAG, "Unexpected empty event queue in processEventBlock!", new Exception());
   242             return;
   243         }
   245         // the odd loop condition is because the first event in the queue will
   246         // always be a DOWN or POINTER_DOWN event, and we want to process all
   247         // the events in the queue starting at that one, up to but not including
   248         // the next DOWN or POINTER_DOWN event.
   250         MotionEvent event = mEventQueue.poll();
   251         while (true) {
   252             // event being null here is valid and represents a block of events
   253             // that has already been dispatched.
   255             if (event != null) {
   256                 dispatchEvent(event, allowDefaultAction);
   257                 event.recycle();
   258                 event = null;
   259             }
   260             if (mEventQueue.isEmpty()) {
   261                 // we have processed the backlog of events, and are all caught up.
   262                 // now we can set clear the hold flag and set the dispatch flag so
   263                 // that the handleEvent() function can do the right thing for all
   264                 // remaining events in this block (which is still ongoing) without
   265                 // having to put them in the queue.
   266                 mHoldInQueue = false;
   267                 mAllowDefaultAction = allowDefaultAction;
   268                 break;
   269             }
   270             event = mEventQueue.peek();
   271             if (event == null || isDownEvent(event)) {
   272                 // we have finished processing the block we were interested in.
   273                 // now we wait for the next call to processEventBlock
   274                 if (event != null) {
   275                     mPanZoomController.startingNewEventBlock(event, true);
   276                 }
   277                 break;
   278             }
   279             // pop the event we peeked above, as it is still part of the block and
   280             // we want to keep processing
   281             mEventQueue.remove();
   282         }
   283     }
   285     private class ListenerTimeoutProcessor implements Runnable {
   286         /* This MUST be run on the UI thread */
   287         @Override
   288         public void run() {
   289             if (mProcessingBalance < 0) {
   290                 // gecko already responded with default-prevented notification, and so
   291                 // the block of events this ListenerTimeoutProcessor corresponds to have
   292                 // already been removed from the queue.
   293             } else {
   294                 processEventBlock(true);
   295             }
   296             mProcessingBalance++;
   297         }
   298     }
   300     // Tabs.OnTabsChangedListener implementation
   302     @Override
   303     public void onTabChanged(Tab tab, Tabs.TabEvents msg, Object data) {
   304         if ((Tabs.getInstance().isSelectedTab(tab) && msg == Tabs.TabEvents.STOP) || msg == Tabs.TabEvents.SELECTED) {
   305             mWaitForTouchListeners = tab.getHasTouchListeners();
   306         }
   307     }
   308 }

mercurial