tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

EventDispatcher.java (18927B)


      1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
      2 * vim: ts=4 sw=4 expandtab:
      3 /* This Source Code Form is subject to the terms of the Mozilla Public
      4 * License, v. 2.0. If a copy of the MPL was not distributed with this
      5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      6 
      7 package org.mozilla.gecko;
      8 
      9 import android.os.Handler;
     10 import android.util.Log;
     11 import androidx.annotation.AnyThread;
     12 import java.util.ArrayDeque;
     13 import java.util.Arrays;
     14 import java.util.Deque;
     15 import java.util.HashMap;
     16 import java.util.HashSet;
     17 import java.util.Map;
     18 import java.util.Set;
     19 import org.mozilla.gecko.annotation.ReflectionTarget;
     20 import org.mozilla.gecko.annotation.RobocopTarget;
     21 import org.mozilla.gecko.annotation.WrapForJNI;
     22 import org.mozilla.gecko.mozglue.JNIObject;
     23 import org.mozilla.gecko.util.BundleEventListener;
     24 import org.mozilla.gecko.util.EventCallback;
     25 import org.mozilla.gecko.util.GeckoBundle;
     26 import org.mozilla.gecko.util.ThreadUtils;
     27 import org.mozilla.geckoview.BuildConfig;
     28 import org.mozilla.geckoview.GeckoResult;
     29 
     30 @RobocopTarget
     31 public final class EventDispatcher extends JNIObject {
     32  private static final String LOGTAG = "GeckoEventDispatcher";
     33 
     34  private static final EventDispatcher INSTANCE = new EventDispatcher();
     35 
     36  /**
     37   * The capacity of a HashMap is rounded up to the next power-of-2. Every time the size of the map
     38   * goes beyond 75% of the capacity, the map is rehashed. Therefore, to empirically determine the
     39   * initial capacity that avoids rehashing, we need to determine the initial size, divide it by
     40   * 75%, and round up to the next power-of-2.
     41   */
     42  private static final int DEFAULT_UI_EVENTS_COUNT = 128; // Empirically measured
     43 
     44  private static class Message {
     45    final String type;
     46    final GeckoBundle bundle;
     47    final EventCallback callback;
     48 
     49    Message(final String type, final GeckoBundle bundle, final EventCallback callback) {
     50      this.type = type;
     51      this.bundle = bundle;
     52      this.callback = callback;
     53    }
     54  }
     55 
     56  // GeckoBundle-based events.
     57  private final MultiMap<String, BundleEventListener> mListeners =
     58      new MultiMap<>(DEFAULT_UI_EVENTS_COUNT);
     59  private Deque<Message> mPendingMessages = new ArrayDeque<>();
     60 
     61  private boolean mAttachedToGecko;
     62  private final NativeQueue mNativeQueue;
     63  private final String mName;
     64 
     65  private static Map<String, EventDispatcher> sDispatchers = new HashMap<>();
     66 
     67  @ReflectionTarget
     68  @WrapForJNI(calledFrom = "gecko")
     69  public static EventDispatcher getInstance() {
     70    return INSTANCE;
     71  }
     72 
     73  /**
     74   * Gets a named EventDispatcher.
     75   *
     76   * <p>Named EventDispatchers can be used to communicate to Gecko's corresponding named
     77   * EventDispatcher.
     78   *
     79   * <p>Messages for named EventDispatcher are queued by default when no listener is present. Queued
     80   * messages will be released automatically when a listener is attached.
     81   *
     82   * <p>A named EventDispatcher needs to be disposed manually by calling {@link #shutdown} when it
     83   * is not needed anymore.
     84   *
     85   * @param name Name for this EventDispatcher.
     86   * @return the existing named EventDispatcher for a given name or a newly created one if it
     87   *     doesn't exist.
     88   */
     89  @ReflectionTarget
     90  @WrapForJNI(calledFrom = "gecko")
     91  public static EventDispatcher byName(final String name) {
     92    synchronized (sDispatchers) {
     93      EventDispatcher dispatcher = sDispatchers.get(name);
     94 
     95      if (dispatcher == null) {
     96        dispatcher = new EventDispatcher(name);
     97        sDispatchers.put(name, dispatcher);
     98      }
     99 
    100      return dispatcher;
    101    }
    102  }
    103 
    104  /* package */ EventDispatcher() {
    105    mNativeQueue = GeckoThread.getNativeQueue();
    106    mName = null;
    107  }
    108 
    109  /* package */ EventDispatcher(final String name) {
    110    mNativeQueue = GeckoThread.getNativeQueue();
    111    mName = name;
    112  }
    113 
    114  public EventDispatcher(final NativeQueue queue) {
    115    mNativeQueue = queue;
    116    mName = null;
    117  }
    118 
    119  private boolean isReadyForDispatchingToGecko() {
    120    return mNativeQueue.isReady();
    121  }
    122 
    123  @WrapForJNI
    124  @Override // JNIObject
    125  protected native void disposeNative();
    126 
    127  @WrapForJNI(stubName = "Shutdown")
    128  protected native void shutdownNative();
    129 
    130  @WrapForJNI private static final int DETACHED = 0;
    131  @WrapForJNI private static final int ATTACHED = 1;
    132  @WrapForJNI private static final int REATTACHING = 2;
    133 
    134  @WrapForJNI(calledFrom = "gecko")
    135  private synchronized void setAttachedToGecko(final int state) {
    136    if (mAttachedToGecko && state == DETACHED) {
    137      dispose(false);
    138    }
    139    mAttachedToGecko = (state == ATTACHED);
    140  }
    141 
    142  /**
    143   * Shuts down this EventDispatcher and release resources.
    144   *
    145   * <p>Only named EventDispatcher can be shut down manually. A shut down EventDispatcher will not
    146   * receive any further messages.
    147   */
    148  public void shutdown() {
    149    if (mName == null) {
    150      throw new RuntimeException("Only named EventDispatcher's can be shut down.");
    151    }
    152 
    153    mAttachedToGecko = false;
    154    shutdownNative();
    155    dispose(false);
    156 
    157    synchronized (sDispatchers) {
    158      sDispatchers.put(mName, null);
    159    }
    160  }
    161 
    162  private void dispose(final boolean force) {
    163    final Handler geckoHandler = ThreadUtils.sGeckoHandler;
    164    if (geckoHandler == null) {
    165      return;
    166    }
    167 
    168    geckoHandler.post(
    169        new Runnable() {
    170          @Override
    171          public void run() {
    172            if (force || !mAttachedToGecko) {
    173              disposeNative();
    174            }
    175          }
    176        });
    177  }
    178 
    179  public void registerUiThreadListener(final BundleEventListener listener, final String... events) {
    180    try {
    181      synchronized (mListeners) {
    182        for (final String event : events) {
    183          if (!BuildConfig.RELEASE_OR_BETA && mListeners.containsEntry(event, listener)) {
    184            throw new IllegalStateException("Already registered " + event);
    185          }
    186          mListeners.add(event, listener);
    187        }
    188        flush(events);
    189      }
    190    } catch (final Exception e) {
    191      throw new IllegalArgumentException("Invalid new list type", e);
    192    }
    193  }
    194 
    195  public void unregisterUiThreadListener(
    196      final BundleEventListener listener, final String... events) {
    197    synchronized (mListeners) {
    198      for (final String event : events) {
    199        if (!mListeners.remove(event, listener) && !BuildConfig.RELEASE_OR_BETA) {
    200          throw new IllegalArgumentException(event + " was not registered");
    201        }
    202      }
    203    }
    204  }
    205 
    206  @WrapForJNI
    207  private native boolean hasGeckoListener(final String event);
    208 
    209  @WrapForJNI(dispatchTo = "gecko")
    210  private native void dispatchToGecko(
    211      final String event, final GeckoBundle data, final EventCallback callback);
    212 
    213  /**
    214   * Dispatch event to any registered Bundle listeners (non-Gecko thread listeners).
    215   *
    216   * @param type Event type
    217   * @param message Bundle message
    218   */
    219  public void dispatch(final String type, final GeckoBundle message) {
    220    dispatch(type, message, /* callback */ null);
    221  }
    222 
    223  private abstract class CallbackResult<T> extends GeckoResult<T> implements EventCallback {
    224    @Override
    225    public void sendError(final Object response) {
    226      completeExceptionally(new QueryException(response));
    227    }
    228  }
    229 
    230  public static class QueryException extends Exception {
    231    public final Object data;
    232 
    233    public QueryException(final Object data) {
    234      this.data = data;
    235    }
    236  }
    237 
    238  /**
    239   * Query event to any registered Bundle listeners (non-Gecko thread listeners).
    240   *
    241   * <p>The returned GeckoResult completes when the event handler returns.
    242   *
    243   * @param type Event type
    244   */
    245  public GeckoResult<Void> queryVoid(final String type) {
    246    return queryVoid(type, null);
    247  }
    248 
    249  /**
    250   * Query event to any registered Bundle listeners (non-Gecko thread listeners).
    251   *
    252   * <p>The returned GeckoResult completes when the event handler returns.
    253   *
    254   * @param type Event type
    255   * @param message GeckoBundle message
    256   */
    257  public GeckoResult<Void> queryVoid(final String type, final GeckoBundle message) {
    258    return query(type, message);
    259  }
    260 
    261  /**
    262   * Query event to any registered Bundle listeners (non-Gecko thread listeners).
    263   *
    264   * <p>The returned GeckoResult completes with the given boolean value returned by the handler.
    265   *
    266   * @param type Event type
    267   */
    268  public GeckoResult<Boolean> queryBoolean(final String type) {
    269    return queryBoolean(type, null);
    270  }
    271 
    272  /**
    273   * Query event to any registered Bundle listeners (non-Gecko thread listeners).
    274   *
    275   * <p>The returned GeckoResult completes with the given boolean value returned by the handler.
    276   *
    277   * @param type Event type
    278   * @param message GeckoBundle message
    279   */
    280  public GeckoResult<Boolean> queryBoolean(final String type, final GeckoBundle message) {
    281    return query(type, message);
    282  }
    283 
    284  /**
    285   * Query event to any registered Bundle listeners (non-Gecko thread listeners).
    286   *
    287   * <p>The returned GeckoResult completes with the given String value returned by the handler.
    288   *
    289   * @param type Event type
    290   */
    291  public GeckoResult<String> queryString(final String type) {
    292    return queryString(type, null);
    293  }
    294 
    295  /**
    296   * Query event to any registered Bundle listeners (non-Gecko thread listeners).
    297   *
    298   * <p>The returned GeckoResult completes with the given String value returned by the handler.
    299   *
    300   * @param type Event type
    301   * @param message GeckoBundle message
    302   */
    303  public GeckoResult<String> queryString(final String type, final GeckoBundle message) {
    304    return query(type, message);
    305  }
    306 
    307  /**
    308   * Query event to any registered Bundle listeners (non-Gecko thread listeners).
    309   *
    310   * <p>The returned GeckoResult completes with the given {@link GeckoBundle} value returned by the
    311   * handler.
    312   *
    313   * @param type Event type
    314   */
    315  public GeckoResult<GeckoBundle> queryBundle(final String type) {
    316    return queryBundle(type, null);
    317  }
    318 
    319  /**
    320   * Query event to any registered Bundle listeners (non-Gecko thread listeners).
    321   *
    322   * <p>The returned GeckoResult completes with the given {@link GeckoBundle} value returned by the
    323   * handler.
    324   *
    325   * @param type Event type
    326   * @param message GeckoBundle message
    327   */
    328  public GeckoResult<GeckoBundle> queryBundle(final String type, final GeckoBundle message) {
    329    return query(type, message);
    330  }
    331 
    332  private <T> GeckoResult<T> query(final String type, final GeckoBundle message) {
    333    final CallbackResult<T> result =
    334        new CallbackResult<T>() {
    335          @Override
    336          @SuppressWarnings("unchecked") // Not a lot we can do about this :(
    337          public void sendSuccess(final Object response) {
    338            complete((T) response);
    339          }
    340        };
    341 
    342    dispatch(type, message, result);
    343    return result;
    344  }
    345 
    346  /**
    347   * Flushes pending messages of given types.
    348   *
    349   * <p>All unhandled messages are put into a pending state by default for named EventDispatcher
    350   * obtained from {@link #byName}.
    351   *
    352   * @param types Types of message to flush.
    353   */
    354  private void flush(final String[] types) {
    355    final Set<String> typeSet = new HashSet<>(Arrays.asList(types));
    356 
    357    final Deque<Message> pendingMessages;
    358    synchronized (mPendingMessages) {
    359      pendingMessages = mPendingMessages;
    360      mPendingMessages = new ArrayDeque<>(pendingMessages.size());
    361    }
    362 
    363    Message message;
    364    while (!pendingMessages.isEmpty()) {
    365      message = pendingMessages.removeFirst();
    366      if (typeSet.contains(message.type)) {
    367        dispatchToThreads(message.type, message.bundle, message.callback);
    368      } else {
    369        synchronized (mPendingMessages) {
    370          mPendingMessages.addLast(message);
    371        }
    372      }
    373    }
    374  }
    375 
    376  /**
    377   * Dispatch event to any registered Bundle listeners (non-Gecko thread listeners).
    378   *
    379   * @param type Event type
    380   * @param message Bundle message
    381   * @param callback Optional object for callbacks from events.
    382   */
    383  @AnyThread
    384  private void dispatch(
    385      final String type, final GeckoBundle message, final EventCallback callback) {
    386    final boolean isGeckoReady;
    387    synchronized (this) {
    388      isGeckoReady = isReadyForDispatchingToGecko();
    389      if (isGeckoReady && mAttachedToGecko && hasGeckoListener(type)) {
    390        dispatchToGecko(type, message, JavaCallbackDelegate.wrap(callback));
    391        return;
    392      }
    393    }
    394 
    395    dispatchToThreads(type, message, callback, isGeckoReady);
    396  }
    397 
    398  @WrapForJNI(calledFrom = "gecko")
    399  private boolean dispatchToThreads(
    400      final String type, final GeckoBundle message, final EventCallback callback) {
    401    return dispatchToThreads(type, message, callback, /* isGeckoReady */ true);
    402  }
    403 
    404  private boolean dispatchToThreads(
    405      final String type,
    406      final GeckoBundle message,
    407      final EventCallback callback,
    408      final boolean isGeckoReady) {
    409    // We need to hold the lock throughout dispatching, to ensure the listeners list
    410    // is consistent, while we iterate over it. We don't have to worry about listeners
    411    // running for a long time while we have the lock, because the listeners will run
    412    // on a separate thread.
    413    synchronized (mListeners) {
    414      if (mListeners.containsKey(type)) {
    415        // Use a delegate to make sure callbacks happen on a specific thread.
    416        final EventCallback wrappedCallback = JavaCallbackDelegate.wrap(callback);
    417 
    418        // Event listeners will call | callback.sendError | if applicable.
    419        for (final BundleEventListener listener : mListeners.get(type)) {
    420          ThreadUtils.getUiHandler()
    421              .post(
    422                  new Runnable() {
    423                    @Override
    424                    public void run() {
    425                      final Double startTime = GeckoJavaSampler.tryToGetProfilerTime();
    426                      listener.handleMessage(type, message, wrappedCallback);
    427                      GeckoJavaSampler.addMarker(
    428                          "EventDispatcher handleMessage", startTime, null, type);
    429                    }
    430                  });
    431        }
    432        return true;
    433      }
    434    }
    435 
    436    if (!isGeckoReady) {
    437      // Usually, we discard an event if there is no listeners for it by
    438      // the time of the dispatch. However, if Gecko(View) is not ready and
    439      // there is no listener for this event that's possibly headed to
    440      // Gecko, we make a special exception to queue this event until
    441      // Gecko(View) is ready. This way, Gecko can first register its
    442      // listeners, and accept the event when it is ready.
    443      mNativeQueue.queueUntilReady(
    444          this,
    445          "dispatchToGecko",
    446          String.class,
    447          type,
    448          GeckoBundle.class,
    449          message,
    450          EventCallback.class,
    451          JavaCallbackDelegate.wrap(callback));
    452      return true;
    453    }
    454 
    455    // Named EventDispatchers use pending messages
    456    if (mName != null) {
    457      synchronized (mPendingMessages) {
    458        mPendingMessages.addLast(new Message(type, message, callback));
    459      }
    460      return true;
    461    }
    462 
    463    final String error = "No listener for " + type;
    464    if (callback != null) {
    465      callback.sendError(error);
    466    }
    467 
    468    Log.w(LOGTAG, error);
    469    return false;
    470  }
    471 
    472  @WrapForJNI
    473  public boolean hasListener(final String event) {
    474    synchronized (mListeners) {
    475      return mListeners.containsKey(event);
    476    }
    477  }
    478 
    479  @Override
    480  protected void finalize() throws Throwable {
    481    dispose(true);
    482  }
    483 
    484  private static class NativeCallbackDelegate extends JNIObject implements EventCallback {
    485    @WrapForJNI(calledFrom = "gecko")
    486    private NativeCallbackDelegate() {}
    487 
    488    @Override // JNIObject
    489    protected void disposeNative() {
    490      // We dispose in finalize().
    491      throw new UnsupportedOperationException();
    492    }
    493 
    494    @WrapForJNI(dispatchTo = "proxy")
    495    @Override // EventCallback
    496    public native void sendSuccess(Object response);
    497 
    498    @WrapForJNI(dispatchTo = "proxy")
    499    @Override // EventCallback
    500    public native void sendError(Object response);
    501 
    502    @WrapForJNI(dispatchTo = "gecko")
    503    @Override // Object
    504    protected native void finalize();
    505  }
    506 
    507  private static class JavaCallbackDelegate implements EventCallback {
    508    private final Thread mOriginalThread = Thread.currentThread();
    509    private final EventCallback mCallback;
    510 
    511    public static EventCallback wrap(final EventCallback callback) {
    512      if (callback == null) {
    513        return null;
    514      }
    515      if (callback instanceof NativeCallbackDelegate) {
    516        // NativeCallbackDelegate always posts to Gecko thread if needed.
    517        return callback;
    518      }
    519      return new JavaCallbackDelegate(callback);
    520    }
    521 
    522    JavaCallbackDelegate(final EventCallback callback) {
    523      mCallback = callback;
    524    }
    525 
    526    private void makeCallback(final boolean callSuccess, final Object rawResponse) {
    527      final Object response;
    528      if (rawResponse instanceof Number) {
    529        // There is ambiguity because a number can be converted to either int or
    530        // double, so e.g. the user can be expecting a double when we give it an
    531        // int. To avoid these pitfalls, we disallow all numbers. The workaround
    532        // is to wrap the number in a JS object / GeckoBundle, which supports
    533        // type coersion for numbers.
    534        throw new UnsupportedOperationException("Cannot use number as Java callback result");
    535      } else if (rawResponse != null && rawResponse.getClass().isArray()) {
    536        // Same with arrays.
    537        throw new UnsupportedOperationException("Cannot use arrays as Java callback result");
    538      } else if (rawResponse instanceof Character) {
    539        response = rawResponse.toString();
    540      } else {
    541        response = rawResponse;
    542      }
    543 
    544      // Call back synchronously if we happen to be on the same thread as the thread
    545      // making the original request.
    546      if (ThreadUtils.isOnThread(mOriginalThread)) {
    547        if (callSuccess) {
    548          mCallback.sendSuccess(response);
    549        } else {
    550          mCallback.sendError(response);
    551        }
    552        return;
    553      }
    554 
    555      // Make callback on the thread of the original request, if the original thread
    556      // is the UI or Gecko thread. Otherwise default to the background thread.
    557      final Handler handler =
    558          mOriginalThread == ThreadUtils.getUiThread()
    559              ? ThreadUtils.getUiHandler()
    560              : mOriginalThread == ThreadUtils.sGeckoThread
    561                  ? ThreadUtils.sGeckoHandler
    562                  : ThreadUtils.getBackgroundHandler();
    563      final EventCallback callback = mCallback;
    564 
    565      handler.post(
    566          new Runnable() {
    567            @Override
    568            public void run() {
    569              if (callSuccess) {
    570                callback.sendSuccess(response);
    571              } else {
    572                callback.sendError(response);
    573              }
    574            }
    575          });
    576    }
    577 
    578    @Override // EventCallback
    579    public void sendSuccess(final Object response) {
    580      makeCallback(/* success */ true, response);
    581    }
    582 
    583    @Override // EventCallback
    584    public void sendError(final Object response) {
    585      makeCallback(/* success */ false, response);
    586    }
    587  }
    588 }