hunk.code_window: #import <ReactABI18_0_0/ABI18_0_0RCTLog.h>
   #import "ABI18_0_0BranchLinkProperties+RNBranch.h"
   #import "ABI18_0_0BranchUniversalObject+RNBranch.h"
   #import "ABI18_0_0RNBranchAgingDictionary.h"
   #import "ABI18_0_0RNBranchEventEmitter.h"
   NSString * const ABI18_0_0RNBranchLinkOpenedNotification = @"ABI18_0_0RNBranchLinkOpenedNotification";
   NSString * const ABI18_0_0RNBranchLinkOpenedNotificationErrorKey = @"error";
   NSString * const ABI18_0_0RNBranchLinkOpenedNotificationParamsKey = @"params";
  
hunk.labels: keep keep keep keep add keep keep keep keep
hunk.after_edit: #import "ABI18_0_0EXScope.h"
  
hunk.file_path: ios/versioned-react-native/ABI18_0_0/Exponent/Modules/Api/Standalone/Branch/ABI18_0_0RNBranch.m
hunk.type: add
file: /**
   * Copyright (c) 2015-present, Facebook, Inc.
   * All rights reserved.
   *
   * This source code is licensed under the BSD-style license found in the
   * LICENSE file in the root directory of this source tree. An additional grant
   * of patent rights can be found in the PATENTS file in the same directory.
   */
  
  package com.facebook.react.uimanager.events;
  
  import javax.annotation.Nullable;
  
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.Comparator;
  import java.util.Map;
  import java.util.concurrent.atomic.AtomicInteger;
  
  import android.util.LongSparseArray;
  
  import com.facebook.infer.annotation.Assertions;
  import com.facebook.react.bridge.LifecycleEventListener;
  import com.facebook.react.bridge.ReactApplicationContext;
  import com.facebook.react.bridge.UiThreadUtil;
  import com.facebook.react.common.MapBuilder;
  import com.facebook.react.modules.core.ChoreographerCompat;
  import com.facebook.react.modules.core.ReactChoreographer;
  import com.facebook.systrace.Systrace;
  
  /**
   * Class responsible for dispatching UI events to JS. The main purpose of this class is to act as an
   * intermediary between UI code generating events and JS, making sure we don't send more events than
   * JS can process.
   *
   * To use it, create a subclass of {@link Event} and call {@link #dispatchEvent(Event)} whenever
   * there's a UI event to dispatch.
   *
   * This class works by installing a Choreographer frame callback on the main thread. This callback
   * then enqueues a runnable on the JS thread (if one is not already pending) that is responsible for
   * actually dispatch events to JS. This implementation depends on the properties that
   *   1) FrameCallbacks run after UI events have been processed in Choreographer.java
   *   2) when we enqueue a runnable on the JS queue thread, it won't be called until after any
   *      previously enqueued JS jobs have finished processing
   *
   * If JS is taking a long time processing events, then the UI events generated on the  UI thread can
   * be coalesced into fewer events so that when the runnable runs, we don't overload JS with a ton
   * of events and make it get even farther behind.
   *
   * Ideally, we don't need this and JS is fast enough to process all the events each frame, but bad
   * things happen, including load on CPUs from the system, and we should handle this case well.
   *
   * == Event Cookies ==
   *
   * An event cookie is made up of the event type id, view tag, and a custom coalescing key. Only
   * Events that have the same cookie can be coalesced.
   *
   * Event Cookie Composition:
   * VIEW_TAG_MASK =       0x00000000ffffffff
   * EVENT_TYPE_ID_MASK =  0x0000ffff00000000
   * COALESCING_KEY_MASK = 0xffff000000000000
   */
  public class EventDispatcher implements LifecycleEventListener {
  
    private static final Comparator<Event> EVENT_COMPARATOR = new Comparator<Event>() {
      @Override
      public int compare(Event lhs, Event rhs) {
        if (lhs == null && rhs == null) {
          return 0;
        }
        if (lhs == null) {
          return -1;
        }
        if (rhs == null) {
          return 1;
        }
  
        long diff = lhs.getTimestampMs() - rhs.getTimestampMs();
        if (diff == 0) {
          return 0;
        } else if (diff < 0) {
          return -1;
        } else {
          return 1;
        }
      }
    };
  
    private final Object mEventsStagingLock = new Object();
    private final Object mEventsToDispatchLock = new Object();
    private final ReactApplicationContext mReactContext;
    private final LongSparseArray<Integer> mEventCookieToLastEventIdx = new LongSparseArray<>();
    private final Map<String, Short> mEventNameToEventId = MapBuilder.newHashMap();
    private final DispatchEventsRunnable mDispatchEventsRunnable = new DispatchEventsRunnable();
    private final ArrayList<Event> mEventStaging = new ArrayList<>();
    private final ArrayList<EventDispatcherListener> mListeners = new ArrayList<>();
    private final ScheduleDispatchFrameCallback mCurrentFrameCallback =
      new ScheduleDispatchFrameCallback();
    private final AtomicInteger mHasDispatchScheduledCount = new AtomicInteger();
  
    private Event[] mEventsToDispatch = new Event[16];
    private int mEventsToDispatchSize = 0;
    private volatile @Nullable RCTEventEmitter mRCTEventEmitter;
    private short mNextEventTypeId = 0;
    private volatile boolean mHasDispatchScheduled = false;
  
    public EventDispatcher(ReactApplicationContext reactContext) {
      mReactContext = reactContext;
      mReactContext.addLifecycleEventListener(this);
    }
  
    /**
     * Sends the given Event to JS, coalescing eligible events if JS is backed up.
     */
    public void dispatchEvent(Event event) {
      Assertions.assertCondition(event.isInitialized(), "Dispatched event hasn't been initialized");
  
      for (EventDispatcherListener listener : mListeners) {
        listener.onEventDispatch(event);
      }
  
      synchronized (mEventsStagingLock) {
        mEventStaging.add(event);
        Systrace.startAsyncFlow(
            Systrace.TRACE_TAG_REACT_JAVA_BRIDGE,
            event.getEventName(),
            event.getUniqueID());
      }
      if (mRCTEventEmitter != null) {
        // If the host activity is paused, the frame callback may not be currently
        // posted. Ensure that it is so that this event gets delivered promptly.
        mCurrentFrameCallback.maybePostFromNonUI();
      } else {
        // No JS application has started yet, or resumed. This can happen when a ReactRootView is
        // added to view hierarchy, but ReactContext creation has not completed yet. In this case, any
        // touch event dispatch will hit this codepath, and we simply queue them so that they
        // are dispatched once ReactContext creation completes and JS app is running.
      }
    }
  
    /**
     * Add a listener to this EventDispatcher.
     */
    public void addListener(EventDispatcherListener listener) {
      mListeners.add(listener);
    }
  
    /**
     * Remove a listener from this EventDispatcher.
     */
    public void removeListener(EventDispatcherListener listener) {
      mListeners.remove(listener);
    }
  
    @Override
    public void onHostResume() {
      if (mRCTEventEmitter == null) {
        mRCTEventEmitter = mReactContext.getJSModule(RCTEventEmitter.class);
      }
      mCurrentFrameCallback.maybePostFromNonUI();
    }
  
    @Override
    public void onHostPause() {
      stopFrameCallback();
    }
  
    @Override
    public void onHostDestroy() {
      stopFrameCallback();
    }
  
    public void onCatalystInstanceDestroyed() {
      UiThreadUtil.runOnUiThread(new Runnable() {
        @Override
        public void run() {
          stopFrameCallback();
        }
      });
    }
  
    private void stopFrameCallback() {
      UiThreadUtil.assertOnUiThread();
      mCurrentFrameCallback.stop();
    }
  
    /**
     * We use a staging data structure so that all UI events generated in a single frame are
     * dispatched at once. Otherwise, a JS runnable enqueued in a previous frame could run while the
     * UI thread is in the process of adding UI events and we might incorrectly send one event this
     * frame and another from this frame during the next.
     */
    private void moveStagedEventsToDispatchQueue() {
      synchronized (mEventsStagingLock) {
        synchronized (mEventsToDispatchLock) {
          for (int i = 0; i < mEventStaging.size(); i++) {
            Event event = mEventStaging.get(i);
  
            if (!event.canCoalesce()) {
              addEventToEventsToDispatch(event);
              continue;
            }
  
            long eventCookie = getEventCookie(
                event.getViewTag(),
                event.getEventName(),
                event.getCoalescingKey());
  
            Event eventToAdd = null;
            Event eventToDispose = null;
            Integer lastEventIdx = mEventCookieToLastEventIdx.get(eventCookie);
  
            if (lastEventIdx == null) {
              eventToAdd = event;
              mEventCookieToLastEventIdx.put(eventCookie, mEventsToDispatchSize);
            } else {
              Event lastEvent = mEventsToDispatch[lastEventIdx];
              Event coalescedEvent = event.coalesce(lastEvent);
              if (coalescedEvent != lastEvent) {
                eventToAdd = coalescedEvent;
                mEventCookieToLastEventIdx.put(eventCookie, mEventsToDispatchSize);
                eventToDispose = lastEvent;
                mEventsToDispatch[lastEventIdx] = null;
              } else {
                eventToDispose = event;
              }
            }
  
            if (eventToAdd != null) {
              addEventToEventsToDispatch(eventToAdd);
            }
            if (eventToDispose != null) {
              eventToDispose.dispose();
            }
          }
        }
        mEventStaging.clear();
      }
    }
  
    private long getEventCookie(int viewTag, String eventName, short coalescingKey) {
      short eventTypeId;
      Short eventIdObj = mEventNameToEventId.get(eventName);
      if (eventIdObj != null) {
        eventTypeId = eventIdObj;
      } else {
        eventTypeId = mNextEventTypeId++;
        mEventNameToEventId.put(eventName, eventTypeId);
      }
      return getEventCookie(viewTag, eventTypeId, coalescingKey);
    }
  
    private static long getEventCookie(int viewTag, short eventTypeId, short coalescingKey) {
      return viewTag |
          (((long) eventTypeId) & 0xffff) << 32 |
          (((long) coalescingKey) & 0xffff) << 48;
    }
  
    private class ScheduleDispatchFrameCallback extends ChoreographerCompat.FrameCallback {
      private volatile boolean mIsPosted = false;
      private boolean mShouldStop = false;
  
      @Override
      public void doFrame(long frameTimeNanos) {
        UiThreadUtil.assertOnUiThread();
  
        if (mShouldStop) {
          mIsPosted = false;
        } else {
          post();
        }
  
        Systrace.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "ScheduleDispatchFrameCallback");
        try {
          moveStagedEventsToDispatchQueue();
  
          if (mEventsToDispatchSize > 0 && !mHasDispatchScheduled) {
            mHasDispatchScheduled = true;
            Systrace.startAsyncFlow(
                Systrace.TRACE_TAG_REACT_JAVA_BRIDGE,
                "ScheduleDispatchFrameCallback",
                mHasDispatchScheduledCount.get());
            mReactContext.runOnJSQueueThread(mDispatchEventsRunnable);
          }
        } finally {
          Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE);
        }
      }
  
      public void stop() {
        mShouldStop = true;
      }
  
      public void maybePost() {
        if (!mIsPosted) {
          mIsPosted = true;
          post();
        }
      }
  
      private void post() {
        ReactChoreographer.getInstance()
            .postFrameCallback(ReactChoreographer.CallbackType.TIMERS_EVENTS, mCurrentFrameCallback);
      }
  
      public void maybePostFromNonUI() {
        if (mIsPosted) {
          return;
        }
  
        // We should only hit this slow path when we receive events while the host activity is paused.
        if (mReactContext.isOnUiQueueThread()) {
          maybePost();
        } else {
          mReactContext.runOnUiQueueThread(new Runnable() {
            @Override
            public void run() {
              maybePost();
            }
          });
        }
      }
    }
  
    private class DispatchEventsRunnable implements Runnable {
  
      @Override
      public void run() {
        Systrace.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "DispatchEventsRunnable");
        try {
          Systrace.endAsyncFlow(
              Systrace.TRACE_TAG_REACT_JAVA_BRIDGE,
              "ScheduleDispatchFrameCallback",
              mHasDispatchScheduledCount.getAndIncrement());
          mHasDispatchScheduled = false;
          Assertions.assertNotNull(mRCTEventEmitter);
          synchronized (mEventsToDispatchLock) {
            // We avoid allocating an array and iterator, and "sorting" if we don't need to.
            // This occurs when the size of mEventsToDispatch is zero or one.
            if (mEventsToDispatchSize > 1) {
              Arrays.sort(mEventsToDispatch, 0, mEventsToDispatchSize, EVENT_COMPARATOR);
            }
            for (int eventIdx = 0; eventIdx < mEventsToDispatchSize; eventIdx++) {
              Event event = mEventsToDispatch[eventIdx];
              // Event can be null if it has been coalesced into another event.
              if (event == null) {
                continue;
              }
              Systrace.endAsyncFlow(
                  Systrace.TRACE_TAG_REACT_JAVA_BRIDGE,
                  event.getEventName(),
                  event.getUniqueID());
              event.dispatch(mRCTEventEmitter);
              event.dispose();
            }
            clearEventsToDispatch();
            mEventCookieToLastEventIdx.clear();
          }
        } finally {
          Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE);
        }
      }
    }
  
    private void addEventToEventsToDispatch(Event event) {
      if (mEventsToDispatchSize == mEventsToDispatch.length) {
        mEventsToDispatch = Arrays.copyOf(mEventsToDispatch, 2 * mEventsToDispatch.length);
      }
      mEventsToDispatch[mEventsToDispatchSize++] = event;
    }
  
    private void clearEventsToDispatch() {
      Arrays.fill(mEventsToDispatch, 0, mEventsToDispatchSize, null);
      mEventsToDispatchSize = 0;
    }
  }
  