diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index f96a609fbe7afc..f928f8a25abbb1 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -3265,10 +3265,12 @@ public abstract interface class com/facebook/react/modules/core/RCTNativeAppEven } public final class com/facebook/react/modules/core/ReactChoreographer { - public static fun getInstance ()Lcom/facebook/react/modules/core/ReactChoreographer; - public static fun initialize (Lcom/facebook/react/internal/ChoreographerProvider;)V - public fun postFrameCallback (Lcom/facebook/react/modules/core/ReactChoreographer$CallbackType;Landroid/view/Choreographer$FrameCallback;)V - public fun removeFrameCallback (Lcom/facebook/react/modules/core/ReactChoreographer$CallbackType;Landroid/view/Choreographer$FrameCallback;)V + public static final field Companion Lcom/facebook/react/modules/core/ReactChoreographer$Companion; + public synthetic fun (Lcom/facebook/react/internal/ChoreographerProvider;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public static final fun getInstance ()Lcom/facebook/react/modules/core/ReactChoreographer; + public static final fun initialize (Lcom/facebook/react/internal/ChoreographerProvider;)V + public final fun postFrameCallback (Lcom/facebook/react/modules/core/ReactChoreographer$CallbackType;Landroid/view/Choreographer$FrameCallback;)V + public final fun removeFrameCallback (Lcom/facebook/react/modules/core/ReactChoreographer$CallbackType;Landroid/view/Choreographer$FrameCallback;)V } public final class com/facebook/react/modules/core/ReactChoreographer$CallbackType : java/lang/Enum { @@ -3277,10 +3279,16 @@ public final class com/facebook/react/modules/core/ReactChoreographer$CallbackTy public static final field NATIVE_ANIMATED_MODULE Lcom/facebook/react/modules/core/ReactChoreographer$CallbackType; public static final field PERF_MARKERS Lcom/facebook/react/modules/core/ReactChoreographer$CallbackType; public static final field TIMERS_EVENTS Lcom/facebook/react/modules/core/ReactChoreographer$CallbackType; + public static fun getEntries ()Lkotlin/enums/EnumEntries; public static fun valueOf (Ljava/lang/String;)Lcom/facebook/react/modules/core/ReactChoreographer$CallbackType; public static fun values ()[Lcom/facebook/react/modules/core/ReactChoreographer$CallbackType; } +public final class com/facebook/react/modules/core/ReactChoreographer$Companion { + public final fun getInstance ()Lcom/facebook/react/modules/core/ReactChoreographer; + public final fun initialize (Lcom/facebook/react/internal/ChoreographerProvider;)V +} + public final class com/facebook/react/modules/core/TimingModule : com/facebook/fbreact/specs/NativeTimingSpec, com/facebook/react/modules/core/JavaScriptTimerExecutor { public static final field Companion Lcom/facebook/react/modules/core/TimingModule$Companion; public static final field NAME Ljava/lang/String; diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/ChoreographerCompat.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/ChoreographerCompat.java deleted file mode 100644 index 10a8d22abaef87..00000000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/ChoreographerCompat.java +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -package com.facebook.react.modules.core; - -import android.view.Choreographer; - -public class ChoreographerCompat { - /** - * @deprecated Use Choreographer.FrameCallback instead - */ - @Deprecated - public abstract static class FrameCallback implements Choreographer.FrameCallback {} -} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/ChoreographerCompat.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/ChoreographerCompat.kt new file mode 100644 index 00000000000000..0b24e15e8f11b2 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/ChoreographerCompat.kt @@ -0,0 +1,16 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.modules.core + +import android.view.Choreographer + +public open class ChoreographerCompat { + + @Deprecated("Use Choreographer.FrameCallback instead") + public abstract class FrameCallback : Choreographer.FrameCallback +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/ReactChoreographer.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/ReactChoreographer.java deleted file mode 100644 index a05be50df4b4f6..00000000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/ReactChoreographer.java +++ /dev/null @@ -1,172 +0,0 @@ -/* - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -package com.facebook.react.modules.core; - -import android.view.Choreographer; -import androidx.annotation.Nullable; -import com.facebook.common.logging.FLog; -import com.facebook.infer.annotation.Assertions; -import com.facebook.react.bridge.UiThreadUtil; -import com.facebook.react.common.ReactConstants; -import com.facebook.react.internal.ChoreographerProvider; -import java.util.ArrayDeque; - -/** - * A simple wrapper around Choreographer that allows us to control the order certain callbacks are - * executed within a given frame. The wrapped Choreographer instance will always be the main thread - * one and the API's are safe to use from any thread. - */ -public final class ReactChoreographer { - - public enum CallbackType { - - /** For use by perf markers that need to happen immediately after draw */ - PERF_MARKERS(0), - - /** For use by {@link com.facebook.react.uimanager.UIManagerModule} */ - DISPATCH_UI(1), - - /** For use by {@link com.facebook.react.animated.NativeAnimatedModule} */ - NATIVE_ANIMATED_MODULE(2), - - /** Events that make JS do things. */ - TIMERS_EVENTS(3), - - /** - * Event used to trigger the idle callback. Called after all UI work has been dispatched to JS. - */ - IDLE_EVENT(4), - ; - - private final int mOrder; - - CallbackType(int order) { - mOrder = order; - } - - /*package*/ int getOrder() { - return mOrder; - } - } - - private static ReactChoreographer sInstance; - - public static void initialize(ChoreographerProvider choreographerProvider) { - if (sInstance == null) { - sInstance = new ReactChoreographer(choreographerProvider); - } - } - - public static ReactChoreographer getInstance() { - Assertions.assertNotNull(sInstance, "ReactChoreographer needs to be initialized."); - return sInstance; - } - - private @Nullable ChoreographerProvider.Choreographer mChoreographer; - - private final ArrayDeque[] mCallbackQueues; - - private final Choreographer.FrameCallback mFrameCallback = - new Choreographer.FrameCallback() { - @Override - public void doFrame(long frameTimeNanos) { - synchronized (mCallbackQueues) { - // Callbacks run once and are then automatically removed, the callback will - // be posted again from postFrameCallback - mHasPostedCallback = false; - - for (int i = 0; i < mCallbackQueues.length; i++) { - ArrayDeque callbackQueue = mCallbackQueues[i]; - int initialLength = callbackQueue.size(); - for (int callback = 0; callback < initialLength; callback++) { - Choreographer.FrameCallback frameCallback = callbackQueue.pollFirst(); - if (frameCallback != null) { - frameCallback.doFrame(frameTimeNanos); - mTotalCallbacks--; - } else { - FLog.e(ReactConstants.TAG, "Tried to execute non-existent frame callback"); - } - } - } - maybeRemoveFrameCallback(); - } - } - }; - - private int mTotalCallbacks = 0; - private boolean mHasPostedCallback = false; - - private ReactChoreographer(ChoreographerProvider choreographerProvider) { - mCallbackQueues = new ArrayDeque[CallbackType.values().length]; - for (int i = 0; i < mCallbackQueues.length; i++) { - mCallbackQueues[i] = new ArrayDeque<>(); - } - - UiThreadUtil.runOnUiThread( - () -> { - mChoreographer = choreographerProvider.getChoreographer(); - }); - } - - public void postFrameCallback(CallbackType type, Choreographer.FrameCallback frameCallback) { - synchronized (mCallbackQueues) { - mCallbackQueues[type.getOrder()].addLast(frameCallback); - mTotalCallbacks++; - Assertions.assertCondition(mTotalCallbacks > 0); - - if (!mHasPostedCallback) { - if (mChoreographer == null) { - // Schedule on the main thread, at which point the constructor's async work will have - // completed - UiThreadUtil.runOnUiThread( - () -> { - synchronized (mCallbackQueues) { - postFrameCallbackOnChoreographer(); - } - }); - } else { - postFrameCallbackOnChoreographer(); - } - } - } - } - - /** - * This method writes on mHasPostedCallback and it should be called from another method that has - * the lock mCallbackQueues - */ - private void postFrameCallbackOnChoreographer() { - mChoreographer.postFrameCallback(mFrameCallback); - mHasPostedCallback = true; - } - - public void removeFrameCallback(CallbackType type, Choreographer.FrameCallback frameCallback) { - synchronized (mCallbackQueues) { - if (mCallbackQueues[type.getOrder()].removeFirstOccurrence(frameCallback)) { - mTotalCallbacks--; - maybeRemoveFrameCallback(); - } else { - FLog.e(ReactConstants.TAG, "Tried to remove non-existent frame callback"); - } - } - } - - /** - * This method reads and writes on mHasPostedCallback and it should be called from another method - * that already has the lock on mCallbackQueues. - */ - private void maybeRemoveFrameCallback() { - Assertions.assertCondition(mTotalCallbacks >= 0); - if (mTotalCallbacks == 0 && mHasPostedCallback) { - if (mChoreographer != null) { - mChoreographer.removeFrameCallback(mFrameCallback); - } - mHasPostedCallback = false; - } - } -} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/ReactChoreographer.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/ReactChoreographer.kt new file mode 100644 index 00000000000000..fc27e30e55f84b --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/ReactChoreographer.kt @@ -0,0 +1,143 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.modules.core + +import android.view.Choreographer +import com.facebook.common.logging.FLog +import com.facebook.infer.annotation.Assertions +import com.facebook.react.bridge.UiThreadUtil +import com.facebook.react.common.ReactConstants +import com.facebook.react.common.annotations.VisibleForTesting +import com.facebook.react.internal.ChoreographerProvider +import java.util.ArrayDeque + +/** + * A simple wrapper around Choreographer that allows us to control the order certain callbacks are + * executed within a given frame. The wrapped Choreographer instance will always be the main thread + * one and the API's are safe to use from any thread. + */ +public class ReactChoreographer private constructor(choreographerProvider: ChoreographerProvider) { + public enum class CallbackType(internal val order: Int) { + /** For use by perf markers that need to happen immediately after draw */ + PERF_MARKERS(0), + /** For use by [com.facebook.react.uimanager.UIManagerModule] */ + DISPATCH_UI(1), + /** For use by [com.facebook.react.animated.NativeAnimatedModule] */ + NATIVE_ANIMATED_MODULE(2), + /** Events that make JS do things. */ + TIMERS_EVENTS(3), + /** + * Event used to trigger the idle callback. Called after all UI work has been dispatched to JS. + */ + IDLE_EVENT(4) + } + + private var choreographer: ChoreographerProvider.Choreographer? = null + private val callbackQueues: Array> = + Array(CallbackType.entries.size) { ArrayDeque() } + private var totalCallbacks = 0 + private var hasPostedCallback = false + + private val frameCallback = + Choreographer.FrameCallback { frameTimeNanos -> + synchronized(callbackQueues) { + + // Callbacks run once and are then automatically removed, the callback will + // be posted again from postFrameCallback + hasPostedCallback = false + for (i in callbackQueues.indices) { + val callbackQueue = callbackQueues[i] + val initialLength = callbackQueue.size + for (callback in 0 until initialLength) { + val frameCallback = callbackQueue.pollFirst() + if (frameCallback != null) { + frameCallback.doFrame(frameTimeNanos) + totalCallbacks-- + } else { + FLog.e(ReactConstants.TAG, "Tried to execute non-existent frame callback") + } + } + } + maybeRemoveFrameCallback() + } + } + + init { + UiThreadUtil.runOnUiThread { choreographer = choreographerProvider.getChoreographer() } + } + + public fun postFrameCallback(type: CallbackType, callback: Choreographer.FrameCallback) { + synchronized(callbackQueues) { + callbackQueues[type.order].addLast(callback) + totalCallbacks++ + Assertions.assertCondition(totalCallbacks > 0) + if (!hasPostedCallback) { + if (choreographer == null) { + // Schedule on the main thread, at which point the constructor's async work will have + // completed + UiThreadUtil.runOnUiThread { + synchronized(callbackQueues) { postFrameCallbackOnChoreographer() } + } + } else { + postFrameCallbackOnChoreographer() + } + } + } + } + + public fun removeFrameCallback(type: CallbackType, frameCallback: Choreographer.FrameCallback?) { + synchronized(callbackQueues) { + if (callbackQueues[type.order].removeFirstOccurrence(frameCallback)) { + totalCallbacks-- + maybeRemoveFrameCallback() + } else { + FLog.e(ReactConstants.TAG, "Tried to remove non-existent frame callback") + } + } + } + + /** + * This method writes on mHasPostedCallback and it should be called from another method that has + * the lock on [callbackQueues]. + */ + private fun postFrameCallbackOnChoreographer() { + choreographer?.postFrameCallback(frameCallback) + hasPostedCallback = true + } + + /** + * This method reads and writes on mHasPostedCallback and it should be called from another method + * that already has the lock on [callbackQueues]. + */ + private fun maybeRemoveFrameCallback() { + Assertions.assertCondition(totalCallbacks >= 0) + if (totalCallbacks == 0 && hasPostedCallback) { + choreographer?.removeFrameCallback(frameCallback) + hasPostedCallback = false + } + } + + public companion object { + private var choreographer: ReactChoreographer? = null + + @JvmStatic + public fun initialize(choreographerProvider: ChoreographerProvider) { + if (choreographer == null) { + choreographer = ReactChoreographer(choreographerProvider) + } + } + + @JvmStatic + public fun getInstance(): ReactChoreographer = + checkNotNull(choreographer) { "ReactChoreographer needs to be initialized." } + + @VisibleForTesting + internal fun overrideInstanceForTest(instance: ReactChoreographer?): ReactChoreographer? = + choreographer.also { choreographer = instance } + } +} diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/fabric/events/TouchEventDispatchTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/fabric/events/TouchEventDispatchTest.kt index 05cdc9372ca426..18d9cb9c93b4e5 100644 --- a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/fabric/events/TouchEventDispatchTest.kt +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/fabric/events/TouchEventDispatchTest.kt @@ -464,7 +464,7 @@ class TouchEventDispatchTest { private lateinit var eventDispatcher: EventDispatcher private lateinit var uiManager: FabricUIManager private lateinit var arguments: MockedStatic - private lateinit var reactChoreographer: MockedStatic + private var reactChoreographerOriginal: ReactChoreographer? = null @Before fun setUp() { @@ -492,16 +492,13 @@ class TouchEventDispatchTest { // Ignore scheduled choreographer work val reactChoreographerMock = mock(ReactChoreographer::class.java) - reactChoreographer = mockStatic(ReactChoreographer::class.java) - reactChoreographer - .`when` { ReactChoreographer.getInstance() } - .thenReturn(reactChoreographerMock) + reactChoreographerOriginal = ReactChoreographer.overrideInstanceForTest(reactChoreographerMock) } @After fun tearDown() { arguments.close() - reactChoreographer.close() + ReactChoreographer.overrideInstanceForTest(reactChoreographerOriginal) } @Test diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/modules/timing/TimingModuleTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/modules/timing/TimingModuleTest.kt index 96437acf867063..2d36284234bfd5 100644 --- a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/modules/timing/TimingModuleTest.kt +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/modules/timing/TimingModuleTest.kt @@ -30,8 +30,7 @@ import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentMatchers.any -import org.mockito.ArgumentMatchers.eq +import org.mockito.ArgumentMatchers import org.mockito.MockedStatic import org.mockito.Mockito.doAnswer import org.mockito.Mockito.doReturn @@ -47,6 +46,19 @@ import org.mockito.stubbing.Answer import org.robolectric.RobolectricTestRunner import org.robolectric.Shadows.shadowOf +public object MockCompat { + // Same as Mockito's 'eq()', but works for non-nullable types + public fun eq(value: T): T = ArgumentMatchers.eq(value) ?: value + + // Same as Mockito's 'any()', but works for non-nullable types + fun any(): T { + ArgumentMatchers.any() + return uninitialized() + } + + @Suppress("UNCHECKED_CAST") fun uninitialized(): T = null as T +} + @RunWith(RobolectricTestRunner::class) class TimingModuleTest { companion object { @@ -56,14 +68,15 @@ class TimingModuleTest { private lateinit var reactContext: BridgeReactContext private lateinit var headlessContext: HeadlessJsTaskContext private lateinit var timingModule: TimingModule - private lateinit var reactChoreographerMock: ReactChoreographer private lateinit var postFrameCallbackHandler: PostFrameCallbackHandler private lateinit var idlePostFrameCallbackHandler: PostFrameCallbackHandler - private var currentTimeNs = 0L private lateinit var jsTimersMock: JSTimers private lateinit var arguments: MockedStatic private lateinit var systemClock: MockedStatic - private lateinit var reactChoreographer: MockedStatic + private lateinit var reactChoreographerMock: ReactChoreographer + + private var currentTimeNs = 0L + private var reactChoreographerOriginal: ReactChoreographer? = null @Before fun prepareModules() { @@ -88,10 +101,7 @@ class TimingModuleTest { } reactChoreographerMock = mock(ReactChoreographer::class.java) - reactChoreographer = mockStatic(ReactChoreographer::class.java) - reactChoreographer - .`when` { ReactChoreographer.getInstance() } - .thenAnswer { reactChoreographerMock } + reactChoreographerOriginal = ReactChoreographer.overrideInstanceForTest(reactChoreographerMock) val reactInstance = mock(CatalystInstance::class.java) reactContext = spy(BridgeReactContext(mock(Context::class.java))) @@ -105,13 +115,13 @@ class TimingModuleTest { whenever( reactChoreographerMock.postFrameCallback( - eq(CallbackType.TIMERS_EVENTS), any(FrameCallback::class.java))) + MockCompat.eq(CallbackType.TIMERS_EVENTS), MockCompat.any())) .thenAnswer { return@thenAnswer postFrameCallbackHandler.answer(it) } whenever( reactChoreographerMock.postFrameCallback( - eq(CallbackType.IDLE_EVENT), any(FrameCallback::class.java))) + MockCompat.eq(CallbackType.IDLE_EVENT), MockCompat.any())) .thenAnswer { return@thenAnswer idlePostFrameCallbackHandler.answer(it) } @@ -127,7 +137,7 @@ class TimingModuleTest { return@doAnswer true }) .`when`(reactContext) - .runOnJSQueueThread(any(Runnable::class.java)) + .runOnJSQueueThread(MockCompat.any()) timingModule.initialize() } @@ -136,7 +146,7 @@ class TimingModuleTest { fun tearDown() { systemClock.close() arguments.close() - reactChoreographer.close() + ReactChoreographer.overrideInstanceForTest(reactChoreographerOriginal) } private fun stepChoreographerFrame() {