/** * 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.modules.core; import javax.annotation.Nullable; import java.util.Comparator; import java.util.PriorityQueue; import java.util.concurrent.atomic.AtomicBoolean; import android.util.SparseArray; import android.view.Choreographer; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.LifecycleEventListener; import com.facebook.react.bridge.WritableArray; import com.facebook.react.uimanager.ReactChoreographer; import com.facebook.react.common.SystemClock; import com.facebook.infer.annotation.Assertions; /** * Native module for JS timer execution. Timers fire on frame boundaries. */ public final class Timing extends ReactContextBaseJavaModule implements LifecycleEventListener { private static class Timer { private final int mCallbackID; private final boolean mRepeat; private final int mInterval; private long mTargetTime; private Timer(int callbackID, long initialTargetTime, int duration, boolean repeat) { mCallbackID = callbackID; mTargetTime = initialTargetTime; mInterval = duration; mRepeat = repeat; } } private class FrameCallback implements Choreographer.FrameCallback { /** * Calls all timers that have expired since the last time this frame callback was called. */ @Override public void doFrame(long frameTimeNanos) { if (isPaused.get()) { return; } long frameTimeMillis = frameTimeNanos / 1000000; WritableArray timersToCall = null; synchronized (mTimerGuard) { while (!mTimers.isEmpty() && mTimers.peek().mTargetTime < frameTimeMillis) { Timer timer = mTimers.poll(); if (timersToCall == null) { timersToCall = Arguments.createArray(); } timersToCall.pushInt(timer.mCallbackID); if (timer.mRepeat) { timer.mTargetTime = frameTimeMillis + timer.mInterval; mTimers.add(timer); } else { mTimerIdsToTimers.remove(timer.mCallbackID); } } } if (timersToCall != null) { Assertions.assertNotNull(mJSTimersModule).callTimers(timersToCall); } Assertions.assertNotNull(mReactChoreographer) .postFrameCallback(ReactChoreographer.CallbackType.TIMERS_EVENTS, this); } } private final Object mTimerGuard = new Object(); private final PriorityQueue<Timer> mTimers; private final SparseArray<Timer> mTimerIdsToTimers; private final AtomicBoolean isPaused = new AtomicBoolean(false); private final FrameCallback mFrameCallback = new FrameCallback(); private @Nullable ReactChoreographer mReactChoreographer; private @Nullable JSTimersExecution mJSTimersModule; private boolean mFrameCallbackPosted = false; public Timing(ReactApplicationContext reactContext) { super(reactContext); // We store timers sorted by finish time. mTimers = new PriorityQueue<Timer>( 11, // Default capacity: for some reason they don't expose a (Comparator) constructor new Comparator<Timer>() { @Override public int compare(Timer lhs, Timer rhs) { long diff = lhs.mTargetTime - rhs.mTargetTime; if (diff == 0) { return 0; } else if (diff < 0) { return -1; } else { return 1; } } }); mTimerIdsToTimers = new SparseArray<Timer>(); } @Override public void initialize() { // Safe to acquire choreographer here, as initialize() is invoked from UI thread. mReactChoreographer = ReactChoreographer.getInstance(); mJSTimersModule = getReactApplicationContext().getCatalystInstance() .getJSModule(JSTimersExecution.class); getReactApplicationContext().addLifecycleEventListener(this); setChoreographerCallback(); } @Override public void onHostPause() { isPaused.set(true); clearChoreographerCallback(); } @Override public void onHostDestroy() { clearChoreographerCallback(); } @Override public void onHostResume() { isPaused.set(false); // TODO(5195192) Investigate possible problems related to restarting all tasks at the same // moment setChoreographerCallback(); } @Override public void onCatalystInstanceDestroy() { clearChoreographerCallback(); } private void setChoreographerCallback() { if (!mFrameCallbackPosted) { Assertions.assertNotNull(mReactChoreographer).postFrameCallback( ReactChoreographer.CallbackType.TIMERS_EVENTS, mFrameCallback); mFrameCallbackPosted = true; } } private void clearChoreographerCallback() { if (mFrameCallbackPosted) { Assertions.assertNotNull(mReactChoreographer).removeFrameCallback( ReactChoreographer.CallbackType.TIMERS_EVENTS, mFrameCallback); mFrameCallbackPosted = false; } } @Override public String getName() { return "RKTiming"; } @ReactMethod public void createTimer( final int callbackID, final int duration, final double jsSchedulingTime, final boolean repeat) { // Adjust for the amount of time it took for native to receive the timer registration call long adjustedDuration = (long) Math.max( 0, jsSchedulingTime - SystemClock.currentTimeMillis() + duration); long initialTargetTime = SystemClock.nanoTime() / 1000000 + adjustedDuration; Timer timer = new Timer(callbackID, initialTargetTime, duration, repeat); synchronized (mTimerGuard) { mTimers.add(timer); mTimerIdsToTimers.put(callbackID, timer); } } @ReactMethod public void deleteTimer(int timerId) { synchronized (mTimerGuard) { Timer timer = mTimerIdsToTimers.get(timerId); if (timer != null) { // We may have already called/removed it mTimerIdsToTimers.remove(timerId); mTimers.remove(timer); } } } }