FRE-600: Fix code review blockers

- Consolidated duplicate UndoManagers to single instance
- Fixed connection promise to only resolve on 'connected' status
- Fixed WebSocketProvider import (WebsocketProvider)
- Added proper doc.destroy() cleanup
- Renamed isPresenceInitialized property to avoid conflict

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
2026-04-25 00:08:01 -04:00
parent 65b552bb08
commit 7c684a42cc
48450 changed files with 5679671 additions and 383 deletions

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
android:versionCode="1"
>
<!-- This is necessary to inform the linter about our min API version. The linter walk the tree
up from the file to lint until it find an AndroidManifest with a minSdkVersion. This is then used
as the min SDK to lint the file.-->
<uses-sdk
android:minSdkVersion="24"
android:targetSdkVersion="36"
/>
</manifest>

View File

@@ -0,0 +1,18 @@
/*
* 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.hermes.instrumentation
internal interface HermesMemoryDumper {
fun shouldSaveSnapshot(): Boolean
fun getInternalStorage(): String
fun getId(): String
fun setMetaData(crashId: String)
}

View File

@@ -0,0 +1,30 @@
/*
* 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.hermes.instrumentation
import com.facebook.soloader.SoLoader
/** Hermes sampling profiler static JSI API. */
public object HermesSamplingProfiler {
init {
SoLoader.loadLibrary("jsijniprofiler")
}
/** Start sample profiling. */
@JvmStatic public external fun enable()
/** Stop sample profiling. */
@JvmStatic public external fun disable()
/**
* Dump sampled stack traces to file.
*
* @param filename the file to dump sampling trace to.
*/
@JvmStatic public external fun dumpSampledTraceToFile(filename: String)
}

View File

@@ -0,0 +1,55 @@
/*
* 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.hermes.reactexecutor
import com.facebook.jni.HybridData
import com.facebook.jni.annotations.DoNotStrip
import com.facebook.react.bridge.JavaScriptExecutor
import com.facebook.react.common.build.ReactBuildConfig
import com.facebook.soloader.SoLoader
public class HermesExecutor internal constructor(enableDebugger: Boolean, debuggerName: String) :
JavaScriptExecutor(initHybridDefaultConfig(enableDebugger, debuggerName)) {
override fun getName(): String = "HermesExecutor$mode"
public companion object {
private var mode: String? = null
init {
loadLibrary()
}
@JvmStatic
@Throws(UnsatisfiedLinkError::class)
public fun loadLibrary() {
if (mode == null) {
// libhermesvm must be loaded explicitly to invoke its JNI_OnLoad.
SoLoader.loadLibrary("hermesvm")
SoLoader.loadLibrary("hermes_executor")
// libhermes_executor is built differently for Debug & Release so we load the proper mode.
mode = if (ReactBuildConfig.DEBUG) "Debug" else "Release"
}
}
@DoNotStrip
@JvmStatic
private external fun initHybridDefaultConfig(
enableDebugger: Boolean,
debuggerName: String,
): HybridData
@DoNotStrip
@JvmStatic
private external fun initHybrid(
enableDebugger: Boolean,
debuggerName: String,
heapSizeMB: Long,
): HybridData
}
}

View File

@@ -0,0 +1,40 @@
/*
* 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.hermes.reactexecutor
import com.facebook.hermes.instrumentation.HermesSamplingProfiler.disable
import com.facebook.hermes.instrumentation.HermesSamplingProfiler.dumpSampledTraceToFile
import com.facebook.hermes.instrumentation.HermesSamplingProfiler.enable
import com.facebook.react.bridge.JavaScriptExecutor
import com.facebook.react.bridge.JavaScriptExecutorFactory
public class HermesExecutorFactory : JavaScriptExecutorFactory {
private var enableDebugger = true
private var debuggerName = ""
public fun setEnableDebugger(enableDebugger: Boolean) {
this.enableDebugger = enableDebugger
}
public fun setDebuggerName(debuggerName: String) {
this.debuggerName = debuggerName
}
override fun create(): JavaScriptExecutor = HermesExecutor(enableDebugger, debuggerName)
override fun startSamplingProfiler() {
enable()
}
override fun stopSamplingProfiler(filename: String) {
dumpSampledTraceToFile(filename)
disable()
}
override fun toString(): String = "JSIExecutor+HermesRuntime"
}

View File

@@ -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.
# For common use cases for the hybrid pattern, keep symbols which may
# be referenced only from C++.
-keepclassmembers class * {
com.facebook.jni.HybridData *;
<init>(com.facebook.jni.HybridData);
}
-keepclasseswithmembers class * {
com.facebook.jni.HybridData *;
}

View File

@@ -0,0 +1,136 @@
/*
* 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
import com.facebook.react.bridge.ModuleHolder
import com.facebook.react.bridge.ModuleSpec
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.internal.featureflags.ReactNativeNewArchitectureFeatureFlags
import com.facebook.react.module.model.ReactModuleInfo
import com.facebook.react.module.model.ReactModuleInfoProvider
import com.facebook.react.uimanager.ViewManager
import java.util.ArrayList
import java.util.NoSuchElementException
import javax.inject.Provider
/** Abstract class that supports lazy loading of NativeModules by default. */
public abstract class BaseReactPackage : ReactPackage {
@Deprecated("Migrate to [BaseReactPackage] and implement [getModule] instead.")
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
throw UnsupportedOperationException(
"createNativeModules method is not supported. Use getModule() method instead."
)
}
/**
* The API needed for TurboModules. Given a module name, it returns an instance of [NativeModule]
* for the name
*
* @param name name of the Native Module
* @param reactContext [ReactApplicationContext] context for this
*/
abstract override fun getModule(
name: String,
reactContext: ReactApplicationContext,
): NativeModule?
/**
* This is a temporary method till we implement TurboModules. Once we implement TurboModules, we
* will be able to directly call [BaseReactPackage#getModule(String, ReactApplicationContext)]
* This method will be removed when TurboModule implementation is complete
*
* @param reactContext [ReactApplicationContext]
* @return
*/
internal fun getNativeModuleIterator(
reactContext: ReactApplicationContext
): Iterable<ModuleHolder> {
val entrySet = getReactModuleInfoProvider().getReactModuleInfos().entries
val entrySetIterator = entrySet.iterator()
// This should ideally be an IteratorConvertor, but we don't have any internal library for it
return Iterable {
object : Iterator<ModuleHolder> {
var nextEntry: Map.Entry<String, ReactModuleInfo>? = null
private fun findNext() {
while (entrySetIterator.hasNext()) {
val entry = entrySetIterator.next()
val reactModuleInfo = entry.value
// This Iterator is used to create the NativeModule registry. The NativeModule
// registry must not have TurboModules. Therefore, if TurboModules are enabled, and
// the current NativeModule is a TurboModule, we need to skip iterating over it.
if (
ReactNativeNewArchitectureFeatureFlags.useTurboModules() &&
reactModuleInfo.isTurboModule
) {
continue
}
nextEntry = entry
return
}
nextEntry = null
}
override fun hasNext(): Boolean {
if (nextEntry == null) {
findNext()
}
return nextEntry != null
}
override fun next(): ModuleHolder {
if (nextEntry == null) {
findNext()
}
val entry = nextEntry ?: throw NoSuchElementException("ModuleHolder not found")
// Advance iterator
findNext()
return ModuleHolder(entry.value, ModuleHolderProvider(entry.key, reactContext))
}
}
}
}
/**
* @param reactContext react application context that can be used to create View Managers.
* @return list of module specs that can create the View Managers.
*/
protected open fun getViewManagers(reactContext: ReactApplicationContext): List<ModuleSpec> =
emptyList()
override fun createViewManagers(
reactContext: ReactApplicationContext
): List<ViewManager<in Nothing, in Nothing>> {
val viewManagerModuleSpecs = getViewManagers(reactContext)
if (viewManagerModuleSpecs.isNullOrEmpty()) {
return emptyList()
}
val viewManagers: MutableList<ViewManager<*, *>> = ArrayList()
for (moduleSpec in viewManagerModuleSpecs) {
viewManagers.add((moduleSpec.provider.get() as ViewManager<*, *>))
}
return viewManagers
}
public abstract fun getReactModuleInfoProvider(): ReactModuleInfoProvider
private inner class ModuleHolderProvider(
private val name: String,
private val reactContext: ReactApplicationContext,
) : Provider<NativeModule?> {
override fun get(): NativeModule? = getModule(name, reactContext)
}
}

View File

@@ -0,0 +1,53 @@
/*
* 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
import com.facebook.react.bridge.ModuleSpec
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.UIManager
import com.facebook.react.module.annotations.ReactModuleList
import com.facebook.react.module.model.ReactModuleInfoProvider
import com.facebook.react.uimanager.ViewManager
import com.facebook.react.views.debuggingoverlay.DebuggingOverlayManager
/** Package defining core debugging modules and viewManagers e.g. [DebuggingOverlayManager]). */
@ReactModuleList(nativeModules = [])
public class DebugCorePackage public constructor() :
BaseReactPackage(), ViewManagerOnDemandReactPackage {
/** A map of view managers that should be registered with [UIManager] */
private val viewManagersMap: Map<String, ModuleSpec> by
lazy(LazyThreadSafetyMode.NONE) {
mapOf(
DebuggingOverlayManager.REACT_CLASS to
ModuleSpec.viewManagerSpec { DebuggingOverlayManager() }
)
}
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider = ReactModuleInfoProvider {
emptyMap()
}
public override fun getModule(
name: String,
reactContext: ReactApplicationContext,
): NativeModule? = null
public override fun getViewManagers(reactContext: ReactApplicationContext): List<ModuleSpec> =
viewManagersMap.values.toList()
override fun getViewManagerNames(reactContext: ReactApplicationContext): Collection<String> =
viewManagersMap.keys
override fun createViewManager(
reactContext: ReactApplicationContext,
viewManagerName: String,
): ViewManager<*, *>? =
viewManagersMap.getOrDefault(viewManagerName, null)?.provider?.get() as? ViewManager<*, *>
}

View File

@@ -0,0 +1,190 @@
/*
* 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
import android.annotation.SuppressLint
import android.app.Service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.IBinder
import android.os.PowerManager
import android.os.PowerManager.WakeLock
import com.facebook.react.bridge.ReactContext
import com.facebook.react.bridge.UiThreadUtil
import com.facebook.react.internal.featureflags.ReactNativeNewArchitectureFeatureFlags
import com.facebook.react.jstasks.HeadlessJsTaskConfig
import com.facebook.react.jstasks.HeadlessJsTaskContext.Companion.getInstance
import com.facebook.react.jstasks.HeadlessJsTaskEventListener
import java.util.concurrent.CopyOnWriteArraySet
/**
* Base class for running JS without a UI. Generally, you only need to override [getTaskConfig],
* which is called for every [onStartCommand]. The result, if not `null`, is used to run a JS task.
*
* If you need more fine-grained control over how tasks are run, you can override [onStartCommand]
* and call [startTask] depending on your custom logic.
*
* If you're starting a `HeadlessJsTaskService` from a `BroadcastReceiver` (e.g. handling push
* notifications), make sure to call [acquireWakeLockNow] before returning from
* [BroadcastReceiver.onReceive], to make sure the device doesn't go to sleep before the service is
* started.
*/
public abstract class HeadlessJsTaskService : Service(), HeadlessJsTaskEventListener {
private val activeTasks: MutableSet<Int> = CopyOnWriteArraySet()
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val taskConfig = getTaskConfig(intent)
return if (taskConfig != null) {
startTask(taskConfig)
START_REDELIVER_INTENT
} else {
START_NOT_STICKY
}
}
/**
* Called from [onStartCommand] to create a [HeadlessJsTaskConfig] for this intent.
*
* @return a [HeadlessJsTaskConfig] to be used with [startTask], or `null` to ignore this command.
*/
protected open fun getTaskConfig(intent: Intent?): HeadlessJsTaskConfig? = null
override fun onBind(intent: Intent): IBinder? = null
/**
* Start a task. This method handles starting a new React instance if required.
*
* Has to be called on the UI thread.
*
* @param taskConfig describes what task to start and the parameters to pass to it
*/
protected fun startTask(taskConfig: HeadlessJsTaskConfig) {
UiThreadUtil.assertOnUiThread()
acquireWakeLockNow(this)
val context = reactContext
if (context == null) {
createReactContextAndScheduleTask(taskConfig)
} else {
invokeStartTask(context, taskConfig)
}
}
private fun invokeStartTask(reactContext: ReactContext, taskConfig: HeadlessJsTaskConfig) {
val headlessJsTaskContext = getInstance(reactContext)
headlessJsTaskContext.addTaskEventListener(this)
UiThreadUtil.runOnUiThread {
val taskId = headlessJsTaskContext.startTask(taskConfig)
activeTasks.add(taskId)
}
}
override fun onDestroy() {
super.onDestroy()
reactContext?.let { context ->
val headlessJsTaskContext = getInstance(context)
headlessJsTaskContext.removeTaskEventListener(this)
}
wakeLock?.release()
}
override fun onHeadlessJsTaskStart(taskId: Int): Unit = Unit
override fun onHeadlessJsTaskFinish(taskId: Int) {
activeTasks.remove(taskId)
if (activeTasks.isEmpty()) {
stopSelf()
}
}
/**
* Get the [ReactNativeHost] used by this app. By default, assumes [getApplication] is an instance
* of [ReactApplication] and calls [ReactApplication.reactNativeHost].
*
* Override this method if your application class does not implement `ReactApplication` or you
* simply have a different mechanism for storing a `ReactNativeHost`, e.g. as a static field
* somewhere.
*/
@Suppress("DEPRECATION")
protected open val reactNativeHost: ReactNativeHost
get() = (application as ReactApplication).reactNativeHost
/**
* Get the [ReactHost] used by this app. By default, assumes [getApplication] is an instance of
* [ReactApplication] and calls [ReactApplication.reactHost]. This method assumes it is called in
* new architecture and returns null if not.
*/
protected open val reactHost: ReactHost?
get() = (application as ReactApplication).reactHost
protected val reactContext: ReactContext?
get() {
if (ReactNativeNewArchitectureFeatureFlags.enableBridgelessArchitecture()) {
val reactHost =
checkNotNull(reactHost) { "ReactHost is not initialized in New Architecture" }
return reactHost.currentReactContext
} else {
val reactInstanceManager = reactNativeHost.reactInstanceManager
return reactInstanceManager.currentReactContext
}
}
private fun createReactContextAndScheduleTask(taskConfig: HeadlessJsTaskConfig) {
if (ReactNativeNewArchitectureFeatureFlags.enableBridgelessArchitecture()) {
val reactHost = checkNotNull(reactHost)
reactHost.addReactInstanceEventListener(
object : ReactInstanceEventListener {
override fun onReactContextInitialized(context: ReactContext) {
invokeStartTask(context, taskConfig)
reactHost.removeReactInstanceEventListener(this)
}
}
)
reactHost.start()
} else {
val reactInstanceManager = reactNativeHost.reactInstanceManager
reactInstanceManager.addReactInstanceEventListener(
object : ReactInstanceEventListener {
override fun onReactContextInitialized(context: ReactContext) {
invokeStartTask(context, taskConfig)
reactInstanceManager.removeReactInstanceEventListener(this)
}
}
)
reactInstanceManager.createReactContextInBackground()
}
}
public companion object {
private var wakeLock: WakeLock? = null
/**
* Acquire a wake lock to ensure the device doesn't go to sleep while processing background
* tasks.
*/
@JvmStatic
@SuppressLint("WakelockTimeout")
public fun acquireWakeLockNow(context: Context) {
if (wakeLock == null || wakeLock?.isHeld == false) {
val powerManager = checkNotNull(context.getSystemService(POWER_SERVICE) as PowerManager)
wakeLock =
powerManager
.newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK,
HeadlessJsTaskService::class.java.canonicalName,
)
.also { lock ->
lock.setReferenceCounted(false)
lock.acquire()
}
}
}
}
}

View File

@@ -0,0 +1,57 @@
/*
* 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
import android.content.ComponentCallbacks2
import android.content.Context
import android.content.res.Configuration
import com.facebook.react.bridge.MemoryPressureListener
import java.util.concurrent.CopyOnWriteArrayList
/** Translates and routes memory pressure events. */
public class MemoryPressureRouter(context: Context) : ComponentCallbacks2 {
private val listeners = CopyOnWriteArrayList<MemoryPressureListener>()
init {
context.applicationContext.registerComponentCallbacks(this)
}
public fun destroy(context: Context) {
context.applicationContext.unregisterComponentCallbacks(this)
}
/** Add a listener to be notified of memory pressure events. */
public fun addMemoryPressureListener(listener: MemoryPressureListener) {
if (!listeners.contains(listener)) {
listeners.add(listener)
}
}
/** Remove a listener previously added with [addMemoryPressureListener]. */
public fun removeMemoryPressureListener(listener: MemoryPressureListener) {
listeners.remove(listener)
}
public override fun onTrimMemory(level: Int) {
dispatchMemoryPressure(level)
}
public override fun onConfigurationChanged(newConfig: Configuration): Unit = Unit
@Deprecated(
"onLowMemory is deprecated, use onTrimMemory instead.",
ReplaceWith("onTrimMemory(level)"),
)
public override fun onLowMemory(): Unit = Unit
private fun dispatchMemoryPressure(level: Int) {
for (listener in listeners) {
listener.handleMemoryPressure(level)
}
}
}

View File

@@ -0,0 +1,78 @@
/*
* 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.
*/
@file:Suppress("DEPRECATION")
package com.facebook.react
import com.facebook.react.bridge.ModuleHolder
import com.facebook.react.bridge.NativeModuleRegistry
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.common.annotations.internal.LegacyArchitecture
import com.facebook.react.common.annotations.internal.LegacyArchitectureLogLevel
import com.facebook.react.common.annotations.internal.LegacyArchitectureLogger
/** Helper class to build NativeModuleRegistry. */
@LegacyArchitecture(logLevel = LegacyArchitectureLogLevel.ERROR)
@Deprecated(
message = "This class is part of Legacy Architecture and will be removed in a future release",
level = DeprecationLevel.WARNING,
)
public class NativeModuleRegistryBuilder(
private val reactApplicationContext: ReactApplicationContext,
) {
private val modules = HashMap<String, ModuleHolder>()
@Deprecated(
"ReactInstanceManager is not used",
ReplaceWith("NativeModuleRegistryBuilder(reactApplicationContext)"),
)
public constructor(
reactApplicationContext: ReactApplicationContext,
@Suppress("UNUSED_PARAMETER") reactInstanceManager: ReactInstanceManager,
) : this(reactApplicationContext)
public fun processPackage(reactPackage: ReactPackage) {
// We use an iterable instead of an iterator here to ensure thread safety, and that this list
// cannot be modified
val moduleHolders =
if (reactPackage is BaseReactPackage) {
reactPackage.getNativeModuleIterator(reactApplicationContext)
} else {
ReactPackageHelper.getNativeModuleIterator(reactPackage, reactApplicationContext)
}
for (moduleHolder in moduleHolders) {
val name = moduleHolder.name
val existingNativeModule = modules[name]
if (existingNativeModule != null) {
check(moduleHolder.canOverrideExistingModule) {
"""
Native module $name tried to override ${existingNativeModule.className}.
Check the getPackages() method in MainApplication.java, it might be that module is being created twice.
If this was your intention, set canOverrideExistingModule=true. This error may also be present if the
package is present only once in getPackages() but is also automatically added later during build time
by autolinking. Try removing the existing entry and rebuild.
"""
}
}
modules[name] = moduleHolder
}
}
public fun build(): NativeModuleRegistry = NativeModuleRegistry(reactApplicationContext, modules)
private companion object {
init {
LegacyArchitectureLogger.assertLegacyArchitecture(
"NativeModuleRegistryBuilder",
LegacyArchitectureLogLevel.ERROR,
)
}
}
}

View File

@@ -0,0 +1,185 @@
/*
* 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;
import android.content.Intent;
import android.content.res.Configuration;
import android.os.Bundle;
import android.view.KeyEvent;
import androidx.activity.OnBackPressedCallback;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
import com.facebook.react.modules.core.PermissionAwareActivity;
import com.facebook.react.modules.core.PermissionListener;
import com.facebook.react.util.AndroidVersion;
import org.jetbrains.annotations.NotNull;
/** Base Activity for React Native applications. */
public abstract class ReactActivity extends AppCompatActivity
implements DefaultHardwareBackBtnHandler, PermissionAwareActivity {
private final ReactActivityDelegate mDelegate;
// Due to enforced predictive back on targetSdk 36, 'onBackPressed()' is disabled by default.
// Using a workaround to trigger it manually.
private final OnBackPressedCallback mBackPressedCallback =
new OnBackPressedCallback(true) {
@Override
public void handleOnBackPressed() {
setEnabled(false);
onBackPressed();
setEnabled(true);
}
};
protected ReactActivity() {
mDelegate = createReactActivityDelegate();
}
/**
* Returns the name of the main component registered from JavaScript. This is used to schedule
* rendering of the component. e.g. "MoviesApp"
*/
protected @Nullable String getMainComponentName() {
return null;
}
/** Called at construction time, override if you have a custom delegate implementation. */
protected ReactActivityDelegate createReactActivityDelegate() {
return new ReactActivityDelegate(this, getMainComponentName());
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mDelegate.onCreate(savedInstanceState);
if (AndroidVersion.isAtLeastTargetSdk36(this)) {
getOnBackPressedDispatcher().addCallback(this, mBackPressedCallback);
}
}
@Override
protected void onPause() {
super.onPause();
mDelegate.onPause();
}
@Override
protected void onResume() {
super.onResume();
mDelegate.onResume();
}
@Override
protected void onDestroy() {
super.onDestroy();
mDelegate.onDestroy();
}
public @Nullable ReactDelegate getReactDelegate() {
return mDelegate.getReactDelegate();
}
public ReactActivityDelegate getReactActivityDelegate() {
return mDelegate;
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
mDelegate.onActivityResult(requestCode, resultCode, data);
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
return mDelegate.onKeyDown(keyCode, event) || super.onKeyDown(keyCode, event);
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
return mDelegate.onKeyUp(keyCode, event) || super.onKeyUp(keyCode, event);
}
@Override
public boolean onKeyLongPress(int keyCode, KeyEvent event) {
return mDelegate.onKeyLongPress(keyCode, event) || super.onKeyLongPress(keyCode, event);
}
@Override
public void onBackPressed() {
if (!mDelegate.onBackPressed()) {
super.onBackPressed();
}
}
@Override
public void invokeDefaultOnBackPressed() {
// Disabling callback so the fallback logic (finish activity) can run
// as super.onBackPressed() will call all enabled callbacks in the dispatcher.
mBackPressedCallback.setEnabled(false);
super.onBackPressed();
// Re-enable callback to ensure custom back handling works after activity resume
// Without this, the callback remains disabled when the app returns from background
mBackPressedCallback.setEnabled(true);
}
@Override
public void onNewIntent(Intent intent) {
if (!mDelegate.onNewIntent(intent)) {
super.onNewIntent(intent);
}
}
@Override
public void onUserLeaveHint() {
super.onUserLeaveHint();
mDelegate.onUserLeaveHint();
}
@Override
public void requestPermissions(
String[] permissions, int requestCode, PermissionListener listener) {
mDelegate.requestPermissions(permissions, requestCode, listener);
}
@Override
public void onRequestPermissionsResult(
int requestCode, @NotNull String[] permissions, @NotNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
mDelegate.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
mDelegate.onWindowFocusChanged(hasFocus);
}
@Override
public void onConfigurationChanged(@NotNull Configuration newConfig) {
super.onConfigurationChanged(newConfig);
mDelegate.onConfigurationChanged(newConfig);
}
protected final ReactNativeHost getReactNativeHost() {
return mDelegate.getReactNativeHost();
}
protected ReactHost getReactHost() {
return mDelegate.getReactHost();
}
protected final ReactInstanceManager getReactInstanceManager() {
return mDelegate.getReactInstanceManager();
}
protected final void loadApp(String appKey) {
mDelegate.loadApp(appKey);
}
}

View File

@@ -0,0 +1,325 @@
/*
* 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;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.os.Build;
import android.os.Bundle;
import android.view.KeyEvent;
import android.view.Window;
import androidx.annotation.Nullable;
import com.facebook.infer.annotation.Assertions;
import com.facebook.infer.annotation.Nullsafe;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.common.LifecycleState;
import com.facebook.react.interfaces.fabric.ReactSurface;
import com.facebook.react.internal.featureflags.ReactNativeNewArchitectureFeatureFlags;
import com.facebook.react.modules.core.PermissionListener;
import com.facebook.react.views.view.WindowUtilKt;
import com.facebook.systrace.Systrace;
import java.util.Objects;
/**
* Delegate class for {@link ReactActivity}. You can subclass this to provide custom implementations
* for e.g. {@link #getReactNativeHost()}, if your Application class doesn't implement {@link
* ReactApplication}.
*/
@Nullsafe(Nullsafe.Mode.LOCAL)
public class ReactActivityDelegate {
private final @Nullable Activity mActivity;
private final @Nullable String mMainComponentName;
private @Nullable PermissionListener mPermissionListener;
private @Nullable Callback mPermissionsCallback;
private @Nullable ReactDelegate mReactDelegate;
/**
* Prefer using ReactActivity when possible, as it hooks up all Activity lifecycle methods by
* default. It also implements DefaultHardwareBackBtnHandler, which ReactDelegate requires.
*/
@Deprecated
public ReactActivityDelegate(@Nullable Activity activity, @Nullable String mainComponentName) {
mActivity = activity;
mMainComponentName = mainComponentName;
}
public ReactActivityDelegate(
@Nullable ReactActivity activity, @Nullable String mainComponentName) {
mActivity = activity;
mMainComponentName = mainComponentName;
}
/**
* Public API to populate the launch options that will be passed to React. Here you can customize
* the values that will be passed as 'initialProperties' to the Renderer.
*
* @return Either null or a key-value map as a Bundle
*/
protected @Nullable Bundle getLaunchOptions() {
return null;
}
protected @Nullable Bundle composeLaunchOptions() {
return getLaunchOptions();
}
/**
* Override to customize ReactRootView creation.
*
* <p>Not used on bridgeless
*/
protected @Nullable ReactRootView createRootView() {
return null;
}
/**
* Get the {@link ReactNativeHost} used by this app with Bridge enabled. By default, assumes
* {@link Activity#getApplication()} is an instance of {@link ReactApplication} and calls {@link
* ReactApplication#getReactNativeHost()}. Override this method if your application class does not
* implement {@code ReactApplication} or you simply have a different mechanism for storing a
* {@code ReactNativeHost}, e.g. as a static field somewhere.
*
* @deprecated "Do not access {@link ReactNativeHost} directly. This class is going away in the
* New Architecture. You should access {@link ReactHost} instead."
*/
@Deprecated
protected ReactNativeHost getReactNativeHost() {
return ((ReactApplication) getPlainActivity().getApplication()).getReactNativeHost();
}
/**
* Get the {@link ReactHost} used by this app with Bridgeless enabled. By default, assumes {@link
* Activity#getApplication()} is an instance of {@link ReactApplication} and calls {@link
* ReactApplication#getReactHost()}. Override this method if your application class does not
* implement {@code ReactApplication} or you simply have a different mechanism for storing a
* {@code ReactHost}, e.g. as a static field somewhere.
*/
public @Nullable ReactHost getReactHost() {
return ((ReactApplication) getPlainActivity().getApplication()).getReactHost();
}
protected @Nullable ReactDelegate getReactDelegate() {
return mReactDelegate;
}
/**
* @deprecated @deprecated "Do not access {@link ReactInstanceManager} directly. This class is
* going away in the New Architecture. You should access {@link ReactHost} instead."
* @noinspection deprecation
*/
@Deprecated
public ReactInstanceManager getReactInstanceManager() {
return Objects.requireNonNull(mReactDelegate).getReactInstanceManager();
}
@Nullable
public String getMainComponentName() {
return mMainComponentName;
}
public void onCreate(@Nullable Bundle savedInstanceState) {
Systrace.traceSection(
Systrace.TRACE_TAG_REACT,
"ReactActivityDelegate.onCreate::init",
() -> {
String mainComponentName = getMainComponentName();
final Bundle launchOptions = composeLaunchOptions();
if (mActivity != null) {
Window window = mActivity.getWindow();
if (window != null) {
if (WindowUtilKt.isEdgeToEdgeFeatureFlagOn()) {
WindowUtilKt.enableEdgeToEdge(window);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && isWideColorGamutEnabled()) {
window.setColorMode(ActivityInfo.COLOR_MODE_WIDE_COLOR_GAMUT);
}
}
}
if (ReactNativeNewArchitectureFeatureFlags.enableBridgelessArchitecture()) {
mReactDelegate =
new ReactDelegate(
getPlainActivity(), getReactHost(), mainComponentName, launchOptions);
} else {
mReactDelegate =
new ReactDelegate(
getPlainActivity(),
getReactNativeHost(),
mainComponentName,
launchOptions,
isFabricEnabled()) {
@Override
@Nullable
protected ReactRootView createRootView() {
ReactRootView rootView = ReactActivityDelegate.this.createRootView();
if (rootView == null) {
rootView = super.createRootView();
}
return rootView;
}
};
}
if (mainComponentName != null) {
loadApp(mainComponentName);
}
});
}
protected void loadApp(@Nullable String appKey) {
Objects.requireNonNull(mReactDelegate).loadApp(Objects.requireNonNull(appKey));
getPlainActivity().setContentView(mReactDelegate.getReactRootView());
}
public void setReactSurface(ReactSurface reactSurface) {
Objects.requireNonNull(mReactDelegate).setReactSurface(reactSurface);
}
public void setReactRootView(ReactRootView reactRootView) {
Objects.requireNonNull(mReactDelegate).setReactRootView(reactRootView);
}
public void onUserLeaveHint() {
Objects.requireNonNull(mReactDelegate).onUserLeaveHint();
}
public void onPause() {
Objects.requireNonNull(mReactDelegate).onHostPause();
}
public void onResume() {
Objects.requireNonNull(mReactDelegate).onHostResume();
if (mPermissionsCallback != null) {
mPermissionsCallback.invoke();
mPermissionsCallback = null;
}
}
public void onDestroy() {
Objects.requireNonNull(mReactDelegate).onHostDestroy();
}
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
Objects.requireNonNull(mReactDelegate).onActivityResult(requestCode, resultCode, data, true);
}
public boolean onKeyDown(int keyCode, KeyEvent event) {
return Objects.requireNonNull(mReactDelegate).onKeyDown(keyCode, event);
}
public boolean onKeyUp(int keyCode, KeyEvent event) {
return Objects.requireNonNull(mReactDelegate).shouldShowDevMenuOrReload(keyCode, event);
}
public boolean onKeyLongPress(int keyCode, KeyEvent event) {
return Objects.requireNonNull(mReactDelegate).onKeyLongPress(keyCode);
}
public boolean onBackPressed() {
return Objects.requireNonNull(mReactDelegate).onBackPressed();
}
public boolean onNewIntent(@Nullable Intent intent) {
return Objects.requireNonNull(mReactDelegate).onNewIntent(Objects.requireNonNull(intent));
}
public void onWindowFocusChanged(boolean hasFocus) {
Objects.requireNonNull(mReactDelegate).onWindowFocusChanged(hasFocus);
}
public void onConfigurationChanged(Configuration newConfig) {
Objects.requireNonNull(mReactDelegate).onConfigurationChanged(newConfig);
}
public void requestPermissions(
String[] permissions, int requestCode, @Nullable PermissionListener listener) {
mPermissionListener = listener;
getPlainActivity().requestPermissions(permissions, requestCode);
}
public void onRequestPermissionsResult(
final int requestCode, final String[] permissions, final int[] grantResults) {
Callback permissionsCallback =
args -> {
if (mPermissionListener != null
&& mPermissionListener.onRequestPermissionsResult(
requestCode, permissions, grantResults)) {
mPermissionListener = null;
}
};
LifecycleState lifecycle;
if (isFabricEnabled()) {
ReactHost reactHost = getReactHost();
lifecycle = reactHost != null ? reactHost.getLifecycleState() : LifecycleState.BEFORE_CREATE;
} else {
ReactNativeHost reactNativeHost = getReactNativeHost();
if (!reactNativeHost.hasInstance()) {
lifecycle = LifecycleState.BEFORE_CREATE;
} else {
lifecycle = reactNativeHost.getReactInstanceManager().getLifecycleState();
}
}
// If the permission request didn't show a dialog to the user, we can call the callback
// immediately.
// Otherwise, we need to wait until onResume to call it.
if (lifecycle == LifecycleState.RESUMED) {
permissionsCallback.invoke();
return;
}
mPermissionsCallback = permissionsCallback;
}
protected Context getContext() {
return Assertions.assertNotNull(mActivity);
}
protected Activity getPlainActivity() {
return ((Activity) getContext());
}
protected ReactActivity getReactActivity() {
return ((ReactActivity) getContext());
}
/**
* Get the current {@link ReactContext} from ReactHost or ReactInstanceManager
*
* <p>Do not store a reference to this, if the React instance is reloaded or destroyed, this
* context will no longer be valid.
*/
public @Nullable ReactContext getCurrentReactContext() {
return Objects.requireNonNull(mReactDelegate).getCurrentReactContext();
}
/**
* Override this method if you wish to selectively toggle Fabric for a specific surface. This will
* also control if Concurrent Root (React 18) should be enabled or not.
*
* @return true if Fabric is enabled for this Activity, false otherwise.
*/
protected boolean isFabricEnabled() {
return ReactNativeNewArchitectureFeatureFlags.enableFabricRenderer();
}
/**
* Override this method if you wish to selectively toggle wide color gamut for a specific surface.
*
* @return true if wide gamut is enabled for this Activity, false otherwise.
*/
protected boolean isWideColorGamutEnabled() {
return false;
}
}

View File

@@ -0,0 +1,99 @@
/*
* 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
import android.view.KeyEvent
import android.view.View
import com.facebook.react.bridge.ReactContext
import com.facebook.react.bridge.WritableMap
import com.facebook.react.bridge.WritableNativeMap
/** Responsible for dispatching events specific for hardware inputs. */
internal class ReactAndroidHWInputDeviceHelper {
/**
* We keep a reference to the last focused view id so that we can send it as a target for key
* events and be able to send a blur event when focus changes.
*/
private var lastFocusedViewId = View.NO_ID
/** Called from [ReactRootView]. This is the main place the key events are handled. */
fun handleKeyEvent(ev: KeyEvent, context: ReactContext) {
val eventKeyCode = ev.keyCode
val eventKeyAction = ev.action
if (
(eventKeyAction == KeyEvent.ACTION_UP || eventKeyAction == KeyEvent.ACTION_DOWN) &&
KEY_EVENTS_ACTIONS.containsKey(eventKeyCode)
) {
dispatchEvent(context, KEY_EVENTS_ACTIONS[eventKeyCode], lastFocusedViewId, eventKeyAction)
}
}
/** Called from [ReactRootView] when focused view changes. */
fun onFocusChanged(newFocusedView: View, context: ReactContext) {
if (lastFocusedViewId == newFocusedView.id) {
return
}
if (lastFocusedViewId != View.NO_ID) {
dispatchEvent(context, "blur", lastFocusedViewId)
}
lastFocusedViewId = newFocusedView.id
dispatchEvent(context, "focus", newFocusedView.id)
}
/** Called from [ReactRootView] when the whole view hierarchy looses focus. */
fun clearFocus(context: ReactContext) {
if (lastFocusedViewId != View.NO_ID) {
dispatchEvent(context, "blur", lastFocusedViewId)
}
lastFocusedViewId = View.NO_ID
}
private fun dispatchEvent(
context: ReactContext,
eventType: String?,
targetViewId: Int,
eventKeyAction: Int = -1,
) {
val event: WritableMap =
WritableNativeMap().apply {
putString("eventType", eventType)
putInt("eventKeyAction", eventKeyAction)
if (targetViewId != View.NO_ID) {
putInt("tag", targetViewId)
}
}
context.emitDeviceEvent("onHWKeyEvent", event)
}
private companion object {
/**
* Contains a mapping between handled KeyEvents and the corresponding navigation event that
* should be fired when the KeyEvent is received.
*/
private val KEY_EVENTS_ACTIONS: Map<Int, String> =
mapOf(
KeyEvent.KEYCODE_DPAD_CENTER to "select",
KeyEvent.KEYCODE_ENTER to "select",
KeyEvent.KEYCODE_SPACE to "select",
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE to "playPause",
KeyEvent.KEYCODE_MEDIA_REWIND to "rewind",
KeyEvent.KEYCODE_MEDIA_FAST_FORWARD to "fastForward",
KeyEvent.KEYCODE_MEDIA_STOP to "stop",
KeyEvent.KEYCODE_MEDIA_NEXT to "next",
KeyEvent.KEYCODE_MEDIA_PREVIOUS to "previous",
KeyEvent.KEYCODE_DPAD_UP to "up",
KeyEvent.KEYCODE_DPAD_RIGHT to "right",
KeyEvent.KEYCODE_DPAD_DOWN to "down",
KeyEvent.KEYCODE_DPAD_LEFT to "left",
KeyEvent.KEYCODE_INFO to "info",
KeyEvent.KEYCODE_MENU to "menu",
KeyEvent.KEYCODE_CHANNEL_UP to "channelUp",
KeyEvent.KEYCODE_CHANNEL_DOWN to "channelDown",
)
}
}

View File

@@ -0,0 +1,29 @@
/*
* 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
/** Interface that represents an instance of a React Native application */
public interface ReactApplication {
/** Get the default [ReactNativeHost] for this app. */
@Suppress("DEPRECATION")
@Deprecated(
"You should not use ReactNativeHost directly in the New Architecture. Use ReactHost instead.",
ReplaceWith("reactHost"),
)
public val reactNativeHost: ReactNativeHost
get() {
throw RuntimeException("You should not use ReactNativeHost directly in the New Architecture")
}
/**
* Get the default [ReactHost] for this app. This method will be used by the new architecture of
* react native
*/
public val reactHost: ReactHost?
get() = null
}

View File

@@ -0,0 +1,442 @@
/*
* 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
import android.app.Activity
import android.content.Intent
import android.content.res.Configuration
import android.os.Bundle
import android.view.KeyEvent
import com.facebook.react.bridge.ReactContext
import com.facebook.react.bridge.UiThreadUtil.runOnUiThread
import com.facebook.react.devsupport.DoubleTapReloadRecognizer
import com.facebook.react.devsupport.ReleaseDevSupportManager
import com.facebook.react.devsupport.interfaces.DevSupportManager
import com.facebook.react.interfaces.fabric.ReactSurface
import com.facebook.react.internal.featureflags.ReactNativeNewArchitectureFeatureFlags
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler
/**
* A delegate for handling React Application support. This delegate is unaware whether it is used in
* an [Activity] or a [android.app.Fragment].
*/
@Suppress("DEPRECATION")
public open class ReactDelegate {
private val activity: Activity
private var internalReactRootView: ReactRootView? = null
private val mainComponentName: String?
private var launchOptions: Bundle?
private var doubleTapReloadRecognizer: DoubleTapReloadRecognizer?
@Deprecated(
"You should not use ReactNativeHost directly in the New Architecture. Use ReactHost instead.",
ReplaceWith("reactHost"),
)
private var reactNativeHost: ReactNativeHost? = null
public var reactHost: ReactHost? = null
private set
private var reactSurface: ReactSurface? = null
/**
* Override this method if you wish to selectively toggle Fabric for a specific surface. This will
* also control if Concurrent Root (React 18) should be enabled or not.
*
* @return true if Fabric is enabled for this Activity, false otherwise.
*/
protected var isFabricEnabled: Boolean =
ReactNativeNewArchitectureFeatureFlags.enableFabricRenderer()
private set
/**
* Do not use this constructor as it's not accounting for New Architecture at all. You should use
* [ReactDelegate(Activity, ReactNativeHost, String, Bundle, boolean)] as it's the constructor
* used for New Architecture.
*/
@Deprecated(
"Use one of the other constructors instead to account for New Architecture. Deprecated since 0.75.0"
)
public constructor(
activity: Activity,
reactNativeHost: ReactNativeHost?,
appKey: String?,
launchOptions: Bundle?,
) {
this.activity = activity
this.mainComponentName = appKey
this.launchOptions = launchOptions
this.doubleTapReloadRecognizer = DoubleTapReloadRecognizer()
this.reactNativeHost = reactNativeHost
}
public constructor(
activity: Activity,
reactHost: ReactHost?,
appKey: String?,
launchOptions: Bundle?,
) {
this.activity = activity
this.mainComponentName = appKey
this.launchOptions = launchOptions
this.doubleTapReloadRecognizer = DoubleTapReloadRecognizer()
this.reactHost = reactHost
}
@Deprecated("Deprecated since 0.81.0, use one of the other constructors instead.")
public constructor(
activity: Activity,
reactNativeHost: ReactNativeHost?,
appKey: String?,
launchOptions: Bundle?,
fabricEnabled: Boolean,
) {
this.isFabricEnabled = fabricEnabled
this.activity = activity
this.mainComponentName = appKey
this.launchOptions = launchOptions
this.doubleTapReloadRecognizer = DoubleTapReloadRecognizer()
this.reactNativeHost = reactNativeHost
}
private val devSupportManager: DevSupportManager?
get() =
if (
ReactNativeNewArchitectureFeatureFlags.enableBridgelessArchitecture() &&
reactHost?.devSupportManager != null
) {
reactHost?.devSupportManager
} else if (
reactNativeHost?.hasInstance() == true && reactNativeHost?.reactInstanceManager != null
) {
reactNativeHost?.reactInstanceManager?.devSupportManager
} else {
null
}
public fun onHostResume() {
if (activity !is DefaultHardwareBackBtnHandler) {
throw ClassCastException(
"Host Activity `${activity.javaClass.simpleName}` does not implement DefaultHardwareBackBtnHandler"
)
}
if (
ReactNativeNewArchitectureFeatureFlags.enableBridgelessArchitecture() && reactHost != null
) {
reactHost?.onHostResume(activity, activity as DefaultHardwareBackBtnHandler)
} else {
if (reactNativeHost?.hasInstance() == true) {
reactNativeHost
?.reactInstanceManager
?.onHostResume(activity, activity as DefaultHardwareBackBtnHandler)
}
}
}
public fun onUserLeaveHint() {
if (
ReactNativeNewArchitectureFeatureFlags.enableBridgelessArchitecture() && reactHost != null
) {
reactHost?.onHostLeaveHint(activity)
} else {
if (reactNativeHost?.hasInstance() == true) {
reactNativeHost?.reactInstanceManager?.onUserLeaveHint(activity)
}
}
}
public fun onHostPause() {
if (
ReactNativeNewArchitectureFeatureFlags.enableBridgelessArchitecture() && reactHost != null
) {
reactHost?.onHostPause(activity)
} else {
if (reactNativeHost?.hasInstance() == true) {
reactNativeHost?.reactInstanceManager?.onHostPause(activity)
}
}
}
public fun onHostDestroy() {
unloadApp()
if (
ReactNativeNewArchitectureFeatureFlags.enableBridgelessArchitecture() && reactHost != null
) {
reactHost?.onHostDestroy(activity)
} else {
if (reactNativeHost?.hasInstance() == true) {
reactNativeHost?.reactInstanceManager?.onHostDestroy(activity)
}
}
}
public fun onBackPressed(): Boolean {
if (
ReactNativeNewArchitectureFeatureFlags.enableBridgelessArchitecture() && reactHost != null
) {
return reactHost?.onBackPressed() == true
} else if (reactNativeHost?.hasInstance() == true) {
reactNativeHost?.reactInstanceManager?.onBackPressed()
return true
}
return false
}
public fun onNewIntent(intent: Intent): Boolean {
if (
ReactNativeNewArchitectureFeatureFlags.enableBridgelessArchitecture() && reactHost != null
) {
reactHost?.onNewIntent(intent)
return true
} else {
if (reactNativeHost?.hasInstance() == true) {
reactNativeHost?.reactInstanceManager?.onNewIntent(intent)
return true
}
}
return false
}
public fun onActivityResult(
requestCode: Int,
resultCode: Int,
data: Intent?,
shouldForwardToReactInstance: Boolean,
) {
if (
ReactNativeNewArchitectureFeatureFlags.enableBridgelessArchitecture() &&
reactHost != null &&
shouldForwardToReactInstance
) {
reactHost?.onActivityResult(activity, requestCode, resultCode, data)
} else {
if (reactNativeHost?.hasInstance() == true && shouldForwardToReactInstance) {
reactNativeHost
?.reactInstanceManager
?.onActivityResult(activity, requestCode, resultCode, data)
}
}
}
public fun onWindowFocusChanged(hasFocus: Boolean) {
if (
ReactNativeNewArchitectureFeatureFlags.enableBridgelessArchitecture() && reactHost != null
) {
reactHost?.onWindowFocusChange(hasFocus)
} else {
if (reactNativeHost?.hasInstance() == true) {
reactNativeHost?.reactInstanceManager?.onWindowFocusChange(hasFocus)
}
}
}
public fun onConfigurationChanged(newConfig: Configuration?) {
if (
ReactNativeNewArchitectureFeatureFlags.enableBridgelessArchitecture() && reactHost != null
) {
reactHost?.onConfigurationChanged(checkNotNull(activity))
} else {
if (reactNativeHost?.hasInstance() == true) {
getReactInstanceManager().onConfigurationChanged(checkNotNull(activity), newConfig)
}
}
}
public fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
if (
keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD &&
((ReactNativeNewArchitectureFeatureFlags.enableBridgelessArchitecture() &&
reactHost?.devSupportManager != null) ||
(reactNativeHost?.hasInstance() == true &&
reactNativeHost?.useDeveloperSupport == true))
) {
event.startTracking()
return true
}
return false
}
public fun onKeyLongPress(keyCode: Int): Boolean {
if (keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD || keyCode == KeyEvent.KEYCODE_BACK) {
if (
ReactNativeNewArchitectureFeatureFlags.enableBridgelessArchitecture() && reactHost != null
) {
val devSupportManager = reactHost?.devSupportManager
// onKeyLongPress is a Dev API and not supported in RELEASE mode.
if (devSupportManager != null && devSupportManager !is ReleaseDevSupportManager) {
devSupportManager.showDevOptionsDialog()
return true
}
} else {
if (
reactNativeHost?.hasInstance() == true && reactNativeHost?.useDeveloperSupport == true
) {
reactNativeHost?.reactInstanceManager?.showDevOptionsDialog()
return true
}
}
}
return false
}
public fun reload() {
val devSupportManager = devSupportManager ?: return
// Reload in RELEASE mode
if (devSupportManager is ReleaseDevSupportManager) {
// Do not reload the bundle from JS as there is no bundler running in release mode.
if (ReactNativeNewArchitectureFeatureFlags.enableBridgelessArchitecture()) {
reactHost?.reload("ReactDelegate.reload()")
} else {
runOnUiThread {
if (
reactNativeHost?.hasInstance() == true &&
reactNativeHost?.reactInstanceManager != null
) {
reactNativeHost?.reactInstanceManager?.recreateReactContextInBackground()
}
}
}
return
}
// Reload in DEBUG mode
devSupportManager.handleReloadJS()
}
/** Start the React surface with the app key supplied in the [ReactDelegate] constructor. */
public fun loadApp() {
val name = requireNotNull(mainComponentName) { "Cannot loadApp without a main component name." }
loadApp(name)
}
/**
* Start the React surface for the given app key.
*
* @param appKey The ID of the app to load into the surface.
*/
public fun loadApp(appKey: String) {
// With Bridgeless enabled, create and start the surface
if (ReactNativeNewArchitectureFeatureFlags.enableBridgelessArchitecture()) {
val reactHost = reactHost
if (reactSurface == null && reactHost != null) {
reactSurface = reactHost.createSurface(activity, appKey, launchOptions)
}
reactSurface?.start()
} else {
check(internalReactRootView == null) { "Cannot loadApp while app is already running." }
internalReactRootView = createRootView()
if (reactNativeHost != null) {
internalReactRootView?.startReactApplication(
reactNativeHost?.reactInstanceManager,
appKey,
launchOptions,
)
}
}
}
/** Stop the React surface started with [ReactDelegate.loadApp]. */
public fun unloadApp() {
if (ReactNativeNewArchitectureFeatureFlags.enableBridgelessArchitecture()) {
reactSurface?.stop()
reactSurface = null
} else {
if (internalReactRootView != null) {
internalReactRootView?.unmountReactApplication()
internalReactRootView = null
}
}
}
public fun setReactSurface(reactSurface: ReactSurface?) {
this.reactSurface = reactSurface
}
public var reactRootView: ReactRootView?
get() {
return if (ReactNativeNewArchitectureFeatureFlags.enableBridgelessArchitecture()) {
if (reactSurface != null) {
reactSurface?.view as ReactRootView?
} else {
null
}
} else {
internalReactRootView
}
}
set(reactRootView) {
internalReactRootView = reactRootView
}
// Not used in bridgeless
protected open fun createRootView(): ReactRootView? {
val reactRootView = ReactRootView(activity)
reactRootView.setIsFabric(isFabricEnabled)
return reactRootView
}
/**
* Handles delegating the [Activity.onKeyUp] method to determine whether the application should
* show the developer menu or should reload the React Application.
*
* @return true if we consume the event and either show the develop menu or reloaded the
* application.
*/
public fun shouldShowDevMenuOrReload(keyCode: Int, event: KeyEvent?): Boolean {
val devSupportManager = devSupportManager
// shouldShowDevMenuOrReload is a Dev API and not supported in RELEASE mode.
if (
devSupportManager == null ||
!devSupportManager.keyboardShortcutsEnabled ||
devSupportManager is ReleaseDevSupportManager
) {
return false
}
if (keyCode == KeyEvent.KEYCODE_MENU) {
devSupportManager.showDevOptionsDialog()
return true
}
val didDoubleTapR = doubleTapReloadRecognizer?.didDoubleTapR(keyCode, activity.currentFocus)
if (didDoubleTapR == true) {
devSupportManager.handleReloadJS()
return true
}
return false
}
@Deprecated(
"Do not access [ReactInstanceManager] directly. This class is going away in the New Architecture. You should use [ReactHost] instead."
)
public fun getReactInstanceManager(): ReactInstanceManager {
val nonNullReactNativeHost =
checkNotNull(reactNativeHost) {
"Cannot get ReactInstanceManager without a ReactNativeHost."
}
return nonNullReactNativeHost.reactInstanceManager
}
/**
* Get the current [ReactContext] from [ReactHost] or [ReactInstanceManager]
*
* Do not store a reference to this, if the React instance is reloaded or destroyed, this context
* will no longer be valid.
*/
public val currentReactContext: ReactContext?
get() {
return if (ReactNativeNewArchitectureFeatureFlags.enableBridgelessArchitecture()) {
if (reactHost != null) {
reactHost?.currentReactContext
} else {
null
}
} else {
getReactInstanceManager().currentReactContext
}
}
}

View File

@@ -0,0 +1,245 @@
/*
* 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
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import com.facebook.react.internal.featureflags.ReactNativeNewArchitectureFeatureFlags
import com.facebook.react.modules.core.PermissionAwareActivity
import com.facebook.react.modules.core.PermissionListener
/**
* Fragment for creating a React View. This allows the developer to "embed" a React Application
* inside native components such as a Drawer, ViewPager, etc.
*/
public open class ReactFragment : Fragment(), PermissionAwareActivity {
protected lateinit var reactDelegate: ReactDelegate
private var disableHostLifecycleEvents = false
private var permissionListener: PermissionListener? = null
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
var mainComponentName: String? = null
var launchOptions: Bundle? = null
var fabricEnabled = false
arguments?.let { args ->
mainComponentName = args.getString(ARG_COMPONENT_NAME)
launchOptions = args.getBundle(ARG_LAUNCH_OPTIONS)
fabricEnabled = args.getBoolean(ARG_FABRIC_ENABLED)
@Suppress("DEPRECATION")
disableHostLifecycleEvents = args.getBoolean(ARG_DISABLE_HOST_LIFECYCLE_EVENTS)
}
checkNotNull(mainComponentName) { "Cannot loadApp if component name is null" }
reactDelegate =
if (ReactNativeNewArchitectureFeatureFlags.enableBridgelessArchitecture()) {
ReactDelegate(requireActivity(), reactHost, mainComponentName, launchOptions)
} else {
@Suppress("DEPRECATION")
ReactDelegate(
requireActivity(),
reactNativeHost,
mainComponentName,
launchOptions,
fabricEnabled,
)
}
}
/**
* Get the [ReactNativeHost] used by this app. By default, assumes [Activity.getApplication] is an
* instance of [ReactApplication] and calls [ReactApplication.reactNativeHost]. Override this
* method if your application class does not implement `ReactApplication` or you simply have a
* different mechanism for storing a `ReactNativeHost`, e.g. as a static field somewhere.
*/
@Suppress("DEPRECATION")
@Deprecated(
"You should not use ReactNativeHost directly in the New Architecture. Use ReactHost instead.",
ReplaceWith("reactHost"),
)
protected open val reactNativeHost: ReactNativeHost?
get() = (activity?.application as ReactApplication?)?.reactNativeHost
/**
* Get the [ReactHost] used by this app. By default, assumes [Activity.getApplication] is an
* instance of [ReactApplication] and calls [ReactApplication.reactHost]. Override this method if
* your application class does not implement `ReactApplication` or you simply have a different
* mechanism for storing a `ReactHost`, e.g. as a static field somewhere.
*
* If you're using Old Architecture/Bridge Mode, this method should return null as [ReactHost] is
* a Bridgeless-only concept.
*/
protected open val reactHost: ReactHost?
get() = (activity?.application as ReactApplication?)?.reactHost
public override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View? {
reactDelegate.loadApp()
return reactDelegate.reactRootView
}
public override fun onResume() {
super.onResume()
if (!disableHostLifecycleEvents) {
reactDelegate.onHostResume()
}
}
public override fun onPause() {
super.onPause()
if (!disableHostLifecycleEvents) {
reactDelegate.onHostPause()
}
}
public override fun onDestroy() {
super.onDestroy()
if (!disableHostLifecycleEvents) {
reactDelegate.onHostDestroy()
} else {
reactDelegate.unloadApp()
}
}
@Deprecated("Deprecated in Java")
public override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
@Suppress("DEPRECATION") super.onActivityResult(requestCode, resultCode, data)
reactDelegate.onActivityResult(requestCode, resultCode, data, false)
}
/**
* Helper to forward hardware back presses to our React Native Host.
*
* This must be called via a forward from your host Activity.
*/
public open fun onBackPressed(): Boolean = reactDelegate.onBackPressed()
/**
* Helper to forward onKeyUp commands from our host Activity. This allows [ReactFragment] to
* handle double tap reloads and dev menus.
*
* This must be called via a forward from your host Activity.
*
* @param keyCode keyCode
* @param event event
* @return true if we handled onKeyUp
*/
public open fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean =
reactDelegate.shouldShowDevMenuOrReload(keyCode, event)
@Deprecated("Deprecated in Java")
public override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray,
) {
@Suppress("DEPRECATION")
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
permissionListener?.let {
if (it.onRequestPermissionsResult(requestCode, permissions, grantResults)) {
permissionListener = null
}
}
}
override fun checkPermission(permission: String, pid: Int, uid: Int): Int =
activity?.checkPermission(permission, pid, uid) ?: 0
override fun checkSelfPermission(permission: String): Int =
activity?.checkSelfPermission(permission) ?: 0
@Suppress("DEPRECATION")
override fun requestPermissions(
permissions: Array<String>,
requestCode: Int,
listener: PermissionListener?,
) {
permissionListener = listener
requestPermissions(permissions, requestCode)
}
/** Builder class to help instantiate a ReactFragment. */
public class Builder {
public var componentName: String? = null
public var launchOptions: Bundle? = null
public var fabricEnabled: Boolean = false
/**
* Set the Component name for our React Native instance.
*
* @param componentName The name of the component
* @return Builder
*/
public fun setComponentName(componentName: String): Builder {
this.componentName = componentName
return this
}
/**
* Set the Launch Options for our React Native instance.
*
* @param launchOptions launchOptions
* @return Builder
*/
public fun setLaunchOptions(launchOptions: Bundle): Builder {
this.launchOptions = launchOptions
return this
}
public fun build(): ReactFragment = newInstance(componentName, launchOptions, fabricEnabled)
@Deprecated(
"You should not change call ReactFragment.setFabricEnabled. Instead enable the NewArchitecture for the whole application with newArchEnabled=true in your gradle.properties file"
)
public fun setFabricEnabled(fabricEnabled: Boolean): Builder {
this.fabricEnabled = fabricEnabled
return this
}
}
public companion object {
protected const val ARG_COMPONENT_NAME: String = "arg_component_name"
protected const val ARG_LAUNCH_OPTIONS: String = "arg_launch_options"
protected const val ARG_FABRIC_ENABLED: String = "arg_fabric_enabled"
@Deprecated(
"We will remove this and use a different solution for handling Fragment lifecycle events."
)
protected const val ARG_DISABLE_HOST_LIFECYCLE_EVENTS: String =
"arg_disable_host_lifecycle_events"
/**
* @param componentName The name of the react native component
* @param launchOptions The launch options for the react native component
* @param fabricEnabled Flag to enable Fabric for ReactFragment
* @return A new instance of fragment ReactFragment.
*/
private fun newInstance(
componentName: String?,
launchOptions: Bundle?,
fabricEnabled: Boolean,
): ReactFragment {
val args =
Bundle().apply {
putString(ARG_COMPONENT_NAME, componentName)
putBundle(ARG_LAUNCH_OPTIONS, launchOptions)
putBoolean(ARG_FABRIC_ENABLED, fabricEnabled)
}
return ReactFragment().apply { setArguments(args) }
}
}
}

View File

@@ -0,0 +1,213 @@
/*
* 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
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import com.facebook.react.bridge.ReactContext
import com.facebook.react.bridge.queue.ReactQueueConfiguration
import com.facebook.react.common.LifecycleState
import com.facebook.react.devsupport.DevMenuConfiguration
import com.facebook.react.devsupport.interfaces.DevSupportManager
import com.facebook.react.interfaces.TaskInterface
import com.facebook.react.interfaces.fabric.ReactSurface
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler
/**
* A ReactHost is an object that manages a single [com.facebook.react.runtime.ReactInstance]. A
* ReactHost can be constructed without initializing the ReactInstance, and it will continue to
* exist after the instance is destroyed.
*
* The implementation of this interface should be Thread Safe
*/
public interface ReactHost {
/** The current [LifecycleState] for React Host */
public val lifecycleState: LifecycleState
/**
* The current [ReactContext] associated with ReactInstance. It could be nullable if ReactInstance
* hasn't been created.
*/
public val currentReactContext: ReactContext?
// TODO: review if DevSupportManager should be nullable
/** [DevSupportManager] used by this ReactHost */
public val devSupportManager: DevSupportManager?
// TODO: review if possible to remove ReactQueueConfiguration
/** [ReactQueueConfiguration] for caller to post jobs in React Native threads */
public val reactQueueConfiguration: ReactQueueConfiguration?
/** Routes memory pressure events to interested components */
public val memoryPressureRouter: MemoryPressureRouter
/** To be called when back button is pressed */
public fun onBackPressed(): Boolean
// TODO: review why activity is nullable in all of the lifecycle methods
/** To be called when the host activity is resumed. */
public fun onHostResume(
activity: Activity?,
defaultBackButtonImpl: DefaultHardwareBackBtnHandler?,
)
/** To be called when the host activity is resumed. */
public fun onHostResume(activity: Activity?)
/**
* To be called when the host activity is about to go into the background as the result of user
* choice.
*/
public fun onHostLeaveHint(activity: Activity?)
/** To be called when the host activity is paused. */
public fun onHostPause(activity: Activity?)
/** To be called when the host activity is paused. */
public fun onHostPause()
/** To be called when the host activity is destroyed. */
public fun onHostDestroy()
/** To be called when the host activity is destroyed. */
public fun onHostDestroy(activity: Activity?)
/** To be called to create and setup an ReactSurface. */
public fun createSurface(
context: Context,
moduleName: String,
initialProps: Bundle?,
): ReactSurface
/**
* This function can be used to initialize the ReactInstance in a background thread before a
* surface needs to be rendered. It is not necessary to call this function; startSurface() will
* initialize the ReactInstance if it hasn't been preloaded.
*
* @return A Task that completes when the instance is initialized. The task will be faulted if any
* errors occur during initialization, and will be cancelled if ReactHost.destroy() is called
* before it completes.
*/
public fun start(): TaskInterface<Void>
/**
* Entrypoint to reload the ReactInstance. If the ReactInstance is destroying, will wait until
* destroy is finished, before reloading.
*
* @param reason describing why ReactHost is being reloaded (e.g. js error, user tap on reload
* button)
* @return A task that completes when React Native reloads
*/
public fun reload(reason: String): TaskInterface<Void>
/**
* Entrypoint to destroy the ReactInstance. If the ReactInstance is reloading, will wait until
* reload is finished, before destroying.
*
* The destroy operation is asynchronous and the task returned by this method will complete when
* React Native gets destroyed. Note that the destroy operation will execute in multiple threads,
* in particular some of the sub-tasks will run in the UIThread. Calling
* [TaskInterface.waitForCompletion] from the UIThread will lead into a deadlock. Use [destroy]
* passing the onDestroyFinished callback to be notified when React Native gets destroyed.
*
* @param reason describing why ReactHost is being destroyed (e.g. memory pressure)
* @param ex exception that caused the trigger to destroy ReactHost (or null) This exception will
* be used to log properly the cause of destroy operation.
* @return A task that completes when React Native gets destroyed.
*/
public fun destroy(
reason: String,
ex: Exception?,
): TaskInterface<Void>
/**
* Entrypoint to destroy the ReactInstance. If the ReactInstance is reloading, will wait until
* reload is finished, before destroying.
*
* The destroy operation is asynchronous and the task returned by this method will complete when
* React Native gets destroyed. Note that the destroy operation will execute in multiple threads,
* in particular some of the sub-tasks will run in the UIThread. Calling
* [TaskInterface.waitForCompletion] from the UIThread will lead into a deadlock. Use
* onDestroyFinished callback to be notified when React Native gets destroyed.
*
* @param reason describing why ReactHost is being destroyed (e.g. memory pressure)
* @param ex exception that caused the trigger to destroy ReactHost (or null) This exception will
* be used to log properly the cause of destroy operation.
* @param onDestroyFinished callback that will be called when React Native gets destroyed, the
* callback will run on a background thread.
* @return A task that completes when React Native gets destroyed.
*/
public fun destroy(
reason: String,
ex: Exception?,
onDestroyFinished: (instanceDestroyedSuccessfully: Boolean) -> Unit = {},
): TaskInterface<Void>
/**
* Permanently destroys the ReactHost, including the ReactInstance (if any). The application MUST
* NOT call any further methods on an invalidated ReactHost.
*
* Applications where the ReactHost may be destroyed before the end of the process SHOULD call
* invalidate() before releasing the reference to the ReactHost, to ensure resources are freed in
* a timely manner.
*
* NOTE: This method is designed for complex integrations. Integrators MAY instead hold a
* long-lived reference to a single ReactHost for the lifetime of the Application, without ever
* calling invalidate(). This is explicitly allowed.
*/
public fun invalidate()
/* To be called when the host activity receives an activity result. */
public fun onActivityResult(
activity: Activity,
requestCode: Int,
resultCode: Int,
data: Intent?,
)
/* To be called when focus has changed for the hosting window. */
public fun onWindowFocusChange(hasFocus: Boolean)
/* This method will give JS the opportunity to receive intents via Linking. */
public fun onNewIntent(intent: Intent)
public fun onConfigurationChanged(context: Context)
public fun addBeforeDestroyListener(onBeforeDestroy: () -> Unit)
public fun removeBeforeDestroyListener(onBeforeDestroy: () -> Unit)
/** Add a listener to be notified of ReactInstance events. */
public fun addReactInstanceEventListener(listener: ReactInstanceEventListener)
/** Remove a listener previously added with [addReactInstanceEventListener]. */
public fun removeReactInstanceEventListener(listener: ReactInstanceEventListener)
/** Set the DevMenu configuration. */
public fun setDevMenuConfiguration(config: DevMenuConfiguration): Unit = Unit
/** Sets the source of the bundle to be loaded from the file system and reloads the app. */
public fun setBundleSource(filePath: String): Unit = Unit
/**
* Sets the source of the bundle to be loaded from the packager server, updates the packager
* connection and reloads the app.
*
* @param debugServerHost host and port of the server, for example "localhost:8081"
* @param moduleName the module name to load, for example "js/RNTesterApp.android"
* @param queryMapper a function that takes current packager options and returns updated options
*/
public fun setBundleSource(
debugServerHost: String,
moduleName: String,
queryMapper: (Map<String, String>) -> Map<String, String> = { it },
): Unit = Unit
}

View File

@@ -0,0 +1,19 @@
/*
* 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
import com.facebook.react.bridge.ReactContext
/** Interface to subscribe for react instance events */
public interface ReactInstanceEventListener {
/**
* Called when the react context is initialized (all modules registered). Always called on the UI
* thread.
*/
public fun onReactContextInitialized(context: ReactContext)
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,399 @@
/*
* 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.
*/
@file:Suppress("DEPRECATION")
package com.facebook.react
import android.app.Activity
import android.app.Application
import android.content.Context
import com.facebook.common.logging.FLog
import com.facebook.hermes.reactexecutor.HermesExecutor
import com.facebook.hermes.reactexecutor.HermesExecutorFactory
import com.facebook.react.ReactInstanceManager.initializeSoLoaderIfNecessary
import com.facebook.react.bridge.JSBundleLoader
import com.facebook.react.bridge.JSExceptionHandler
import com.facebook.react.bridge.JavaScriptExecutorFactory
import com.facebook.react.bridge.UIManagerProvider
import com.facebook.react.common.LifecycleState
import com.facebook.react.common.SurfaceDelegateFactory
import com.facebook.react.common.annotations.internal.LegacyArchitecture
import com.facebook.react.common.annotations.internal.LegacyArchitectureLogLevel
import com.facebook.react.common.annotations.internal.LegacyArchitectureLogger
import com.facebook.react.devsupport.DefaultDevSupportManagerFactory
import com.facebook.react.devsupport.DevSupportManagerFactory
import com.facebook.react.devsupport.interfaces.DevBundleDownloadListener
import com.facebook.react.devsupport.interfaces.DevLoadingViewManager
import com.facebook.react.devsupport.interfaces.DevSupportManager
import com.facebook.react.devsupport.interfaces.PausedInDebuggerOverlayManager
import com.facebook.react.devsupport.interfaces.RedBoxHandler
import com.facebook.react.internal.ChoreographerProvider
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler
import com.facebook.react.modules.systeminfo.AndroidInfoHelpers
import com.facebook.react.packagerconnection.RequestHandler
/** Builder class for [ReactInstanceManager]. */
@LegacyArchitecture(logLevel = LegacyArchitectureLogLevel.ERROR)
@Deprecated(
message = "This class is part of Legacy Architecture and will be removed in a future release",
level = DeprecationLevel.WARNING,
)
public class ReactInstanceManagerBuilder {
private val packages: MutableList<ReactPackage> = mutableListOf()
private var jsBundleAssetUrl: String? = null
private var jsBundleLoader: JSBundleLoader? = null
private var jsMainModulePath: String? = null
private var application: Application? = null
private var useDeveloperSupport = false
private var devSupportManagerFactory: DevSupportManagerFactory? = null
private var requireActivity = false
private var keepActivity = false
private var initialLifecycleState: LifecycleState? = null
private var jsExceptionHandler: JSExceptionHandler? = null
private var currentActivity: Activity? = null
private var defaultHardwareBackBtnHandler: DefaultHardwareBackBtnHandler? = null
private var redBoxHandler: RedBoxHandler? = null
private var lazyViewManagersEnabled = false
private var devBundleDownloadListener: DevBundleDownloadListener? = null
private var javaScriptExecutorFactory: JavaScriptExecutorFactory? = null
private var minNumShakes = 1
private var minTimeLeftInFrameForNonBatchedOperationMs = -1
private var uiManagerProvider: UIManagerProvider? = null
private var customPackagerCommandHandlers: Map<String, RequestHandler>? = null
private var tmmDelegateBuilder: ReactPackageTurboModuleManagerDelegate.Builder? = null
private var surfaceDelegateFactory: SurfaceDelegateFactory? = null
private var devLoadingViewManager: DevLoadingViewManager? = null
private var choreographerProvider: ChoreographerProvider? = null
private var pausedInDebuggerOverlayManager: PausedInDebuggerOverlayManager? = null
/** Factory for desired implementation of JavaScriptExecutor. */
public fun setJavaScriptExecutorFactory(
javaScriptExecutorFactory: JavaScriptExecutorFactory?
): ReactInstanceManagerBuilder {
this.javaScriptExecutorFactory = javaScriptExecutorFactory
return this
}
public fun setUIManagerProvider(
uiManagerProvider: UIManagerProvider?
): ReactInstanceManagerBuilder {
this.uiManagerProvider = uiManagerProvider
return this
}
/**
* Name of the JS bundle file to be loaded from application's raw assets.
*
* Example: `"index.android.js"`
*/
public fun setBundleAssetName(bundleAssetName: String?): ReactInstanceManagerBuilder {
jsBundleAssetUrl = if (bundleAssetName == null) null else "assets://$bundleAssetName"
jsBundleLoader = null
return this
}
/**
* Path to the JS bundle file to be loaded from the file system.
*
* Example: `"assets://index.android.js"` or `"/sdcard/main.jsbundle"`
*/
public fun setJSBundleFile(jsBundleFile: String): ReactInstanceManagerBuilder {
if (jsBundleFile.startsWith("assets://")) {
jsBundleAssetUrl = jsBundleFile
jsBundleLoader = null
return this
}
return setJSBundleLoader(JSBundleLoader.createFileLoader(jsBundleFile))
}
/**
* Bundle loader to use when setting up JS environment. This supersedes prior invocations of
* [setJSBundleFile] and [setBundleAssetName].
*
* Example: `JSBundleLoader.createFileLoader(application, bundleFile)`
*/
public fun setJSBundleLoader(jsBundleLoader: JSBundleLoader): ReactInstanceManagerBuilder {
this.jsBundleLoader = jsBundleLoader
jsBundleAssetUrl = null
return this
}
/**
* Path to your app's main module on Metro. This is used when reloading JS during development. All
* paths are relative to the root folder the packager is serving files from. Examples:
* `"index.android"` or `"subdirectory/index.android"`
*/
public fun setJSMainModulePath(jsMainModulePath: String): ReactInstanceManagerBuilder {
this.jsMainModulePath = jsMainModulePath
return this
}
public fun addPackage(reactPackage: ReactPackage): ReactInstanceManagerBuilder {
packages.add(reactPackage)
return this
}
public fun addPackages(reactPackages: List<ReactPackage>): ReactInstanceManagerBuilder {
packages.addAll(reactPackages)
return this
}
/** Required. This must be your `Application` instance. */
public fun setApplication(application: Application): ReactInstanceManagerBuilder {
this.application = application
return this
}
public fun setCurrentActivity(activity: Activity): ReactInstanceManagerBuilder {
currentActivity = activity
return this
}
public fun setDefaultHardwareBackBtnHandler(
defaultHardwareBackBtnHandler: DefaultHardwareBackBtnHandler
): ReactInstanceManagerBuilder {
this.defaultHardwareBackBtnHandler = defaultHardwareBackBtnHandler
return this
}
/**
* When `true`, developer options such as JS reloading and debugging are enabled. Note you still
* have to call [showDevOptionsDialog] to show the dev menu, e.g. when the device Menu button is
* pressed.
*/
public fun setUseDeveloperSupport(useDeveloperSupport: Boolean): ReactInstanceManagerBuilder {
this.useDeveloperSupport = useDeveloperSupport
return this
}
/**
* Set the custom [DevSupportManagerFactory]. If not set, will use
* [DefaultDevSupportManagerFactory].
*/
public fun setDevSupportManagerFactory(
devSupportManagerFactory: DevSupportManagerFactory?
): ReactInstanceManagerBuilder {
this.devSupportManagerFactory = devSupportManagerFactory
return this
}
/**
* When `false`, indicates that correct usage of React Native will NOT involve an Activity. For
* the vast majority of Android apps in the ecosystem, this will not need to change. Unless you
* really know what you're doing, you should probably not change this!
*/
public fun setRequireActivity(requireActivity: Boolean): ReactInstanceManagerBuilder {
this.requireActivity = requireActivity
return this
}
public fun setKeepActivity(keepActivity: Boolean): ReactInstanceManagerBuilder {
this.keepActivity = keepActivity
return this
}
/**
* When the [SurfaceDelegateFactory] is provided, it will be used for native modules to get a
* [SurfaceDelegate] to interact with the platform specific surface that they that needs to be
* rendered in. For mobile platform this is default to be null so that these modules will need to
* provide a default surface delegate. One example of such native module is [LogBoxModule], which
* is rendered in mobile platform with [LogBoxDialog], while in VR platform with custom layer
* provided by runtime.
*/
public fun setSurfaceDelegateFactory(
surfaceDelegateFactory: SurfaceDelegateFactory?
): ReactInstanceManagerBuilder {
this.surfaceDelegateFactory = surfaceDelegateFactory
return this
}
/** Sets the Dev Loading View Manager. */
public fun setDevLoadingViewManager(
devLoadingViewManager: DevLoadingViewManager?
): ReactInstanceManagerBuilder {
this.devLoadingViewManager = devLoadingViewManager
return this
}
public fun setPausedInDebuggerOverlayManager(
pausedInDebuggerOverlayManager: PausedInDebuggerOverlayManager?
): ReactInstanceManagerBuilder {
this.pausedInDebuggerOverlayManager = pausedInDebuggerOverlayManager
return this
}
/**
* Sets the initial lifecycle state of the host. For example, if the host is already resumed at
* creation time, we wouldn't expect an onResume call until we get an onPause call.
*/
public fun setInitialLifecycleState(
initialLifecycleState: LifecycleState
): ReactInstanceManagerBuilder {
this.initialLifecycleState = initialLifecycleState
return this
}
/**
* Set the exception handler for all native module calls. If not set, the default
* [DevSupportManager] will be used, which shows a redbox in dev mode and rethrows (crashes the
* app) in prod mode.
*/
public fun setJSExceptionHandler(handler: JSExceptionHandler?): ReactInstanceManagerBuilder {
jsExceptionHandler = handler
return this
}
public fun setRedBoxHandler(redBoxHandler: RedBoxHandler?): ReactInstanceManagerBuilder {
this.redBoxHandler = redBoxHandler
return this
}
public fun setLazyViewManagersEnabled(
lazyViewManagersEnabled: Boolean
): ReactInstanceManagerBuilder {
this.lazyViewManagersEnabled = lazyViewManagersEnabled
return this
}
public fun setDevBundleDownloadListener(
listener: DevBundleDownloadListener?
): ReactInstanceManagerBuilder {
devBundleDownloadListener = listener
return this
}
public fun setMinNumShakes(minNumShakes: Int): ReactInstanceManagerBuilder {
this.minNumShakes = minNumShakes
return this
}
public fun setMinTimeLeftInFrameForNonBatchedOperationMs(
minTimeLeftInFrameForNonBatchedOperationMs: Int
): ReactInstanceManagerBuilder {
this.minTimeLeftInFrameForNonBatchedOperationMs = minTimeLeftInFrameForNonBatchedOperationMs
return this
}
public fun setCustomPackagerCommandHandlers(
customPackagerCommandHandlers: Map<String, RequestHandler>?
): ReactInstanceManagerBuilder {
this.customPackagerCommandHandlers = customPackagerCommandHandlers
return this
}
public fun setReactPackageTurboModuleManagerDelegateBuilder(
builder: ReactPackageTurboModuleManagerDelegate.Builder?
): ReactInstanceManagerBuilder {
tmmDelegateBuilder = builder
return this
}
public fun setChoreographerProvider(
choreographerProvider: ChoreographerProvider?
): ReactInstanceManagerBuilder {
this.choreographerProvider = choreographerProvider
return this
}
/**
* Instantiates a new [ReactInstanceManager]. Before calling [build], the following must be
* called:
* * [setApplication]
* * [setCurrentActivity] if the activity has already resumed
* * [setDefaultHardwareBackBtnHandler] if the activity has already resumed
* * [setJSBundleFile] or [setJSMainModulePath]
*/
public fun build(): ReactInstanceManager {
val application =
checkNotNull(this.application) { "Application property has not been set with this builder" }
if (initialLifecycleState == LifecycleState.RESUMED) {
checkNotNull(currentActivity) {
"Activity needs to be set if initial lifecycle state is resumed"
}
}
check(useDeveloperSupport || jsBundleAssetUrl != null || jsBundleLoader != null) {
"JS Bundle File or Asset URL has to be provided when dev support is disabled"
}
check(jsMainModulePath != null || jsBundleAssetUrl != null || jsBundleLoader != null) {
"Either MainModulePath or JS Bundle File needs to be provided"
}
// We use the name of the device and the app for debugging & metrics
val appName = application.packageName
val deviceName: String = AndroidInfoHelpers.getFriendlyDeviceName()
val safeJSBundleAssetUrl = jsBundleAssetUrl
return ReactInstanceManager(
application,
currentActivity,
defaultHardwareBackBtnHandler,
javaScriptExecutorFactory
?: getDefaultJSExecutorFactory(appName, deviceName, application.applicationContext),
if ((jsBundleLoader == null && safeJSBundleAssetUrl != null))
JSBundleLoader.createAssetLoader(
application,
safeJSBundleAssetUrl,
loadSynchronously = false,
)
else jsBundleLoader,
jsMainModulePath,
packages,
useDeveloperSupport,
devSupportManagerFactory ?: DefaultDevSupportManagerFactory(),
requireActivity,
keepActivity,
checkNotNull(initialLifecycleState) { "Initial lifecycle state was not set" },
jsExceptionHandler,
redBoxHandler,
lazyViewManagersEnabled,
devBundleDownloadListener,
minNumShakes,
minTimeLeftInFrameForNonBatchedOperationMs,
uiManagerProvider,
customPackagerCommandHandlers,
tmmDelegateBuilder,
surfaceDelegateFactory,
devLoadingViewManager,
choreographerProvider,
pausedInDebuggerOverlayManager,
)
}
private fun getDefaultJSExecutorFactory(
appName: String,
deviceName: String,
applicationContext: Context,
): JavaScriptExecutorFactory? {
ReactInstanceManager.initializeSoLoaderIfNecessary(applicationContext)
// Hermes has been enabled by default in OSS since React Native 0.70.
try {
HermesExecutor.loadLibrary()
return HermesExecutorFactory()
} catch (error: UnsatisfiedLinkError) {
FLog.e(
TAG,
"Unable to load Hermes. Your application is not built correctly and will fail to execute",
)
return null
}
}
private companion object {
private val TAG: String = ReactInstanceManagerBuilder::class.java.simpleName
init {
LegacyArchitectureLogger.assertLegacyArchitecture(
"ReactInstanceManagerBuilder",
LegacyArchitectureLogLevel.ERROR,
)
}
}
}

View File

@@ -0,0 +1,251 @@
/*
* 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;
import android.app.Application;
import androidx.annotation.Nullable;
import com.facebook.infer.annotation.Assertions;
import com.facebook.infer.annotation.Nullsafe;
import com.facebook.react.bridge.JSExceptionHandler;
import com.facebook.react.bridge.JavaScriptExecutorFactory;
import com.facebook.react.bridge.ReactMarker;
import com.facebook.react.bridge.ReactMarkerConstants;
import com.facebook.react.bridge.UIManagerProvider;
import com.facebook.react.common.LifecycleState;
import com.facebook.react.common.SurfaceDelegate;
import com.facebook.react.common.SurfaceDelegateFactory;
import com.facebook.react.common.annotations.internal.LegacyArchitecture;
import com.facebook.react.common.annotations.internal.LegacyArchitectureLogLevel;
import com.facebook.react.common.annotations.internal.LegacyArchitectureLogger;
import com.facebook.react.devsupport.DevSupportManagerFactory;
import com.facebook.react.devsupport.interfaces.DevLoadingViewManager;
import com.facebook.react.devsupport.interfaces.PausedInDebuggerOverlayManager;
import com.facebook.react.devsupport.interfaces.RedBoxHandler;
import com.facebook.react.internal.ChoreographerProvider;
import java.util.List;
/**
* Simple class that holds an instance of {@link ReactInstanceManager}. This can be used in your
* {@link Application class} (see {@link ReactApplication}), or as a static field.
*
* @deprecated This class will be replaced by com.facebook.react.ReactHost in the New Architecture.
*/
@Deprecated(
since = "This class is part of Legacy Architecture and will be removed in a future release")
@LegacyArchitecture(logLevel = LegacyArchitectureLogLevel.ERROR)
@Nullsafe(Nullsafe.Mode.LOCAL)
public abstract class ReactNativeHost {
static {
LegacyArchitectureLogger.assertLegacyArchitecture(
"ReactNativeHost", LegacyArchitectureLogLevel.ERROR);
}
private final Application mApplication;
private @Nullable ReactInstanceManager mReactInstanceManager;
protected ReactNativeHost(Application application) {
mApplication = application;
}
/**
* Get the current {@link ReactInstanceManager} instance, or create one.
*
* <p>NOTE: Care must be taken when storing this reference outside of the ReactNativeHost
* lifecycle. The ReactInstanceManager will be invalidated during {@link #clear()}, and may not be
* used again afterwards.
*/
public synchronized ReactInstanceManager getReactInstanceManager() {
if (mReactInstanceManager == null) {
ReactMarker.logMarker(ReactMarkerConstants.INIT_REACT_RUNTIME_START);
ReactMarker.logMarker(ReactMarkerConstants.GET_REACT_INSTANCE_MANAGER_START);
mReactInstanceManager = createReactInstanceManager();
ReactMarker.logMarker(ReactMarkerConstants.GET_REACT_INSTANCE_MANAGER_END);
}
return mReactInstanceManager;
}
/**
* Get whether this holder contains a {@link ReactInstanceManager} instance, or not. I.e. if
* {@link #getReactInstanceManager()} has been called at least once since this object was created
* or {@link #clear()} was called.
*/
public synchronized boolean hasInstance() {
return mReactInstanceManager != null;
}
/**
* Destroy the current instance and invalidate the internal ReactInstanceManager, reclaiming its
* resources and preventing it from being reused.
*/
public synchronized void clear() {
if (mReactInstanceManager != null) {
mReactInstanceManager.invalidate();
mReactInstanceManager = null;
}
}
protected ReactInstanceManager createReactInstanceManager() {
ReactMarker.logMarker(ReactMarkerConstants.BUILD_REACT_INSTANCE_MANAGER_START);
ReactInstanceManagerBuilder builder = getBaseReactInstanceManagerBuilder();
ReactMarker.logMarker(ReactMarkerConstants.BUILD_REACT_INSTANCE_MANAGER_END);
return builder.build();
}
protected ReactInstanceManagerBuilder getBaseReactInstanceManagerBuilder() {
ReactInstanceManagerBuilder builder =
ReactInstanceManager.builder()
.setApplication(mApplication)
.setJSMainModulePath(getJSMainModuleName())
.setUseDeveloperSupport(getUseDeveloperSupport())
.setDevSupportManagerFactory(getDevSupportManagerFactory())
.setDevLoadingViewManager(getDevLoadingViewManager())
.setRequireActivity(getShouldRequireActivity())
.setSurfaceDelegateFactory(getSurfaceDelegateFactory())
.setJSExceptionHandler(getJSExceptionHandler())
.setLazyViewManagersEnabled(getLazyViewManagersEnabled())
.setRedBoxHandler(getRedBoxHandler())
.setJavaScriptExecutorFactory(getJavaScriptExecutorFactory())
.setUIManagerProvider(getUIManagerProvider())
.setInitialLifecycleState(LifecycleState.BEFORE_CREATE)
.setReactPackageTurboModuleManagerDelegateBuilder(
getReactPackageTurboModuleManagerDelegateBuilder())
.setChoreographerProvider(getChoreographerProvider())
.setPausedInDebuggerOverlayManager(getPausedInDebuggerOverlayManager());
for (ReactPackage reactPackage : getPackages()) {
builder.addPackage(reactPackage);
}
String jsBundleFile = getJSBundleFile();
if (jsBundleFile != null) {
builder.setJSBundleFile(jsBundleFile);
} else {
builder.setBundleAssetName(Assertions.assertNotNull(getBundleAssetName()));
}
return builder;
}
/** Get the {@link RedBoxHandler} to send RedBox-related callbacks to. */
protected @Nullable RedBoxHandler getRedBoxHandler() {
return null;
}
protected @Nullable JSExceptionHandler getJSExceptionHandler() {
return null;
}
/** Get the {@link JavaScriptExecutorFactory}. Override this to use a custom Executor. */
protected @Nullable JavaScriptExecutorFactory getJavaScriptExecutorFactory() {
return null;
}
protected @Nullable ReactPackageTurboModuleManagerDelegate.Builder
getReactPackageTurboModuleManagerDelegateBuilder() {
return null;
}
protected final Application getApplication() {
return mApplication;
}
protected @Nullable UIManagerProvider getUIManagerProvider() {
return reactApplicationContext -> null;
}
/** Returns whether or not to treat it as normal if Activity is null. */
public boolean getShouldRequireActivity() {
return true;
}
/**
* Returns whether view managers should be created lazily. See {@link
* ViewManagerOnDemandReactPackage} for details.
*
* @experimental
*/
public boolean getLazyViewManagersEnabled() {
return false;
}
/**
* Return the {@link SurfaceDelegateFactory} used by NativeModules to get access to a {@link
* SurfaceDelegate} to interact with a surface. By default in the mobile platform the {@link
* SurfaceDelegate} it returns is null, and the NativeModule needs to implement its own {@link
* SurfaceDelegate} to decide how it would interact with its own container surface.
*/
public SurfaceDelegateFactory getSurfaceDelegateFactory() {
return new SurfaceDelegateFactory() {
@Override
public @Nullable SurfaceDelegate createSurfaceDelegate(String moduleName) {
return null;
}
};
}
/**
* Get the {@link DevLoadingViewManager}. Override this to use a custom dev loading view manager
*/
protected @Nullable DevLoadingViewManager getDevLoadingViewManager() {
return null;
}
protected @Nullable PausedInDebuggerOverlayManager getPausedInDebuggerOverlayManager() {
return null;
}
/**
* Returns the name of the main module. Determines the URL used to fetch the JS bundle from Metro.
* It is only used when dev support is enabled. This is the first file to be executed once the
* {@link ReactInstanceManager} is created. e.g. "index.android"
*/
protected String getJSMainModuleName() {
return "index.android";
}
/**
* Returns a custom path of the bundle file. This is used in cases the bundle should be loaded
* from a custom path. By default it is loaded from Android assets, from a path specified by
* {@link getBundleAssetName}. e.g. "file://sdcard/myapp_cache/index.android.bundle"
*/
protected @Nullable String getJSBundleFile() {
return null;
}
/**
* Returns the name of the bundle in assets. If this is null, and no file path is specified for
* the bundle, the app will only work with {@code getUseDeveloperSupport} enabled and will always
* try to load the JS bundle from Metro. e.g. "index.android.bundle"
*/
protected @Nullable String getBundleAssetName() {
return "index.android.bundle";
}
/** Returns whether dev mode should be enabled. This enables e.g. the dev menu. */
public abstract boolean getUseDeveloperSupport();
/** Get the {@link DevSupportManagerFactory}. Override this to use a custom dev support manager */
protected @Nullable DevSupportManagerFactory getDevSupportManagerFactory() {
return null;
}
/**
* Returns a list of {@link ReactPackage} used by the app. You'll most likely want to return at
* least the {@code MainReactPackage}. If your app uses additional views or modules besides the
* default ones, you'll want to include more packages here.
*/
protected abstract List<ReactPackage> getPackages();
/**
* Returns a custom implementation of ChoreographerProvider to be used this host. If null - React
* will use default direct android.view.Choreographer-based provider.
*/
protected @Nullable ChoreographerProvider getChoreographerProvider() {
return null;
}
}

View File

@@ -0,0 +1,53 @@
/*
* 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
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.UIManager
import com.facebook.react.common.annotations.StableReactNativeAPI
import com.facebook.react.uimanager.ViewManager
/**
* Main interface for providing additional capabilities to the catalyst framework by couple of
* different means:
* 1. Registering new native modules
* 1. Registering new JS modules that may be accessed from native modules or from other parts of the
* native code (requiring JS modules from the package doesn't mean it will automatically be
* included as a part of the JS bundle, so there should be a corresponding piece of code on JS
* side that will require implementation of that JS module so that it gets bundled)
* 1. Registering custom native views (view managers) and custom event types
* 1. Registering natively packaged assets/resources (e.g. images) exposed to JS
*
* TODO(6788500, 6788507): Implement support for adding custom views, events and resources
*/
public interface ReactPackage {
/**
* @param reactContext react application context that can be used to create modules
* @return list of native modules to register with the newly created catalyst instance This method
* is deprecated in the new Architecture of React Native.
*/
@Deprecated(message = "Migrate to [BaseReactPackage] and implement [getModule] instead.")
public fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> =
emptyList()
/** @return a list of view managers that should be registered with [UIManager] */
public fun createViewManagers(
reactContext: ReactApplicationContext
): List<ViewManager<in Nothing, in Nothing>>
/**
* Given a module name, it returns an instance of [NativeModule] for the name
*
* @param name name of the Native Module
* @param reactContext [ReactApplicationContext] context for this
*/
@StableReactNativeAPI
public fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? = null
}

View File

@@ -0,0 +1,44 @@
/*
* 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
import com.facebook.common.logging.FLog
import com.facebook.react.bridge.ModuleHolder
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.common.ReactConstants
internal object ReactPackageHelper {
/**
* A helper method to iterate over a list of Native Modules and convert them to an iterable.
*
* @param reactPackage
* @param reactApplicationContext
* @return
*/
fun getNativeModuleIterator(
reactPackage: ReactPackage,
reactApplicationContext: ReactApplicationContext,
): Iterable<ModuleHolder> {
FLog.d(
ReactConstants.TAG,
"${reactPackage.javaClass.simpleName} is not a BaseReactPackage, falling back to old version.",
)
@Suppress("DEPRECATION")
val nativeModules = reactPackage.createNativeModules(reactApplicationContext)
return Iterable {
object : Iterator<ModuleHolder> {
var position = 0
override fun next(): ModuleHolder = ModuleHolder(nativeModules[position++])
override fun hasNext(): Boolean = position < nativeModules.size
}
}
}
}

View File

@@ -0,0 +1,225 @@
/*
* 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
import com.facebook.jni.HybridData
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.internal.featureflags.ReactNativeNewArchitectureFeatureFlags
import com.facebook.react.internal.turbomodule.core.TurboModuleManagerDelegate
import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.module.model.ReactModuleInfo
import com.facebook.react.turbomodule.core.interfaces.TurboModule
public abstract class ReactPackageTurboModuleManagerDelegate : TurboModuleManagerDelegate {
internal fun interface ModuleProvider {
fun getModule(moduleName: String): NativeModule?
}
private val moduleProviders = mutableListOf<ModuleProvider>()
private val packageModuleInfos = mutableMapOf<ModuleProvider, Map<String, ReactModuleInfo>>()
private val shouldEnableLegacyModuleInterop =
ReactNativeNewArchitectureFeatureFlags.enableBridgelessArchitecture() &&
ReactNativeNewArchitectureFeatureFlags.useTurboModuleInterop()
protected constructor(
reactApplicationContext: ReactApplicationContext,
packages: List<ReactPackage>,
) : super() {
initialize(reactApplicationContext, packages)
}
protected constructor(
reactApplicationContext: ReactApplicationContext,
packages: List<ReactPackage>,
hybridData: HybridData,
) : super(hybridData) {
initialize(reactApplicationContext, packages)
}
private fun initialize(
reactApplicationContext: ReactApplicationContext,
packages: List<ReactPackage>,
) {
val applicationContext: ReactApplicationContext = reactApplicationContext
for (reactPackage in packages) {
if (reactPackage is BaseReactPackage) {
val moduleProvider = ModuleProvider { moduleName: String ->
reactPackage.getModule(moduleName, applicationContext)
}
moduleProviders.add(moduleProvider)
packageModuleInfos[moduleProvider] =
reactPackage.getReactModuleInfoProvider().getReactModuleInfos()
continue
}
if (shouldSupportLegacyPackages()) {
// TODO(T145105887): Output warnings that ReactPackage was used
@Suppress("DEPRECATION")
val nativeModules = reactPackage.createNativeModules(reactApplicationContext)
val moduleMap: MutableMap<String, NativeModule> = mutableMapOf()
val reactModuleInfoMap: MutableMap<String, ReactModuleInfo> = mutableMapOf()
for (module in nativeModules) {
val moduleClass: Class<out NativeModule> = module.javaClass
val reactModule = moduleClass.getAnnotation(ReactModule::class.java)
val moduleName = reactModule?.name ?: module.name
@Suppress("DEPRECATION")
val moduleInfo: ReactModuleInfo =
if (reactModule != null)
ReactModuleInfo(
moduleName,
moduleClass.name,
reactModule.canOverrideExistingModule,
true,
false,
ReactModuleInfo.classIsTurboModule(moduleClass),
)
else
ReactModuleInfo(
moduleName,
moduleClass.name,
module.canOverrideExistingModule(),
true,
false,
ReactModuleInfo.classIsTurboModule(moduleClass),
)
reactModuleInfoMap[moduleName] = moduleInfo
moduleMap[moduleName] = module
}
val moduleProvider = ModuleProvider { module -> moduleMap[module] }
moduleProviders.add(moduleProvider)
packageModuleInfos[moduleProvider] = reactModuleInfoMap
}
}
}
override fun getModule(moduleName: String): TurboModule? {
var resolvedModule: NativeModule? = null
for (moduleProvider in moduleProviders) {
val moduleInfo: ReactModuleInfo? = packageModuleInfos[moduleProvider]?.get(moduleName)
if (
moduleInfo?.isTurboModule == true &&
(resolvedModule == null || moduleInfo.canOverrideExistingModule)
) {
val module = moduleProvider.getModule(moduleName)
if (module != null) {
resolvedModule = module
}
}
}
// Skip TurboModule-incompatible modules
val isLegacyModule = resolvedModule !is TurboModule
if (isLegacyModule) {
return null
}
return resolvedModule as TurboModule
}
override fun unstable_isModuleRegistered(moduleName: String): Boolean {
for (moduleProvider in moduleProviders) {
val moduleInfo: ReactModuleInfo? = packageModuleInfos[moduleProvider]?.get(moduleName)
if (moduleInfo?.isTurboModule == true) {
return true
}
}
return false
}
override fun unstable_isLegacyModuleRegistered(moduleName: String): Boolean {
for (moduleProvider in moduleProviders) {
val moduleInfo: ReactModuleInfo? = packageModuleInfos[moduleProvider]?.get(moduleName)
if (moduleInfo?.isTurboModule == false) {
return true
}
}
return false
}
override fun getLegacyModule(moduleName: String): NativeModule? {
if (!shouldEnableLegacyModuleInterop) {
return null
}
var resolvedModule: NativeModule? = null
for (moduleProvider in moduleProviders) {
val moduleInfo: ReactModuleInfo? = packageModuleInfos[moduleProvider]?.get(moduleName)
if (
moduleInfo?.isTurboModule == false &&
(resolvedModule == null || moduleInfo.canOverrideExistingModule)
) {
val module = moduleProvider.getModule(moduleName)
if (module != null) {
resolvedModule = module
}
}
}
// Skip TurboModule-compatible modules
val isLegacyModule = resolvedModule !is TurboModule
if (!isLegacyModule) {
return null
}
return resolvedModule
}
override fun getEagerInitModuleNames(): List<String> = buildList {
for (moduleProvider in moduleProviders) {
for (moduleInfo in packageModuleInfos[moduleProvider]?.values ?: emptyList()) {
if (moduleInfo.isTurboModule && moduleInfo.needsEagerInit) {
add(moduleInfo.name)
}
}
}
}
private fun shouldSupportLegacyPackages(): Boolean = shouldEnableLegacyModuleInterop
public abstract class Builder {
private var packages: List<ReactPackage>? = null
private var context: ReactApplicationContext? = null
public fun setPackages(packages: List<ReactPackage>): Builder {
this.packages = packages.toList()
return this
}
public fun setReactApplicationContext(context: ReactApplicationContext?): Builder {
this.context = context
return this
}
protected abstract fun build(
context: ReactApplicationContext,
packages: List<ReactPackage>,
): ReactPackageTurboModuleManagerDelegate
public fun build(): ReactPackageTurboModuleManagerDelegate {
val nonNullContext =
requireNotNull(context) {
"The ReactApplicationContext must be provided to create ReactPackageTurboModuleManagerDelegate"
}
val nonNullPackages =
requireNotNull(packages) {
"A set of ReactPackages must be provided to create ReactPackageTurboModuleManagerDelegate"
}
return build(nonNullContext, nonNullPackages)
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,14 @@
/*
* 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
@Deprecated(
message = "Use BaseReactPackage instead",
replaceWith = ReplaceWith(expression = "BaseReactPackage"),
)
public abstract class TurboReactPackage : BaseReactPackage()

View File

@@ -0,0 +1,77 @@
/*
* 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
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.uimanager.ViewManager
/**
* Interface for React Native packages that provide ViewManagers on-demand rather than eagerly.
*
* This interface enables lazy initialization of ViewManagers, improving startup performance by
* deferring the creation of ViewManager instances until they are actually needed by the JavaScript
* code. Instead of instantiating all ViewManagers during package initialization, implementing
* classes can defer creation until a specific ViewManager is requested by name.
*
* This pattern is particularly beneficial for applications with many ViewManagers, as it reduces
* memory footprint and initialization time by only creating the ViewManagers that are actively
* used.
*
* Implementing classes should maintain a registry or factory mechanism to create ViewManagers based
* on their names when requested.
*
* @see com.facebook.react.uimanager.ViewManager
* @see com.facebook.react.ReactPackage
*/
public interface ViewManagerOnDemandReactPackage {
/**
* Provides the names of all ViewManagers available in this package.
*
* This method returns a collection of ViewManager names that can be accessed from JavaScript. The
* names returned should match the values returned by [ViewManager.getName] for each ViewManager
* that this package can create. The React Native framework uses these names to determine which
* ViewManagers are available and to request their creation on-demand.
*
* This method is called during the initialization phase to register available ViewManagers
* without actually instantiating them, enabling lazy loading.
*
* @param reactContext The React application context, which provides access to the Android
* application context and React Native lifecycle information
* @return A collection of ViewManager names. Returns an empty collection if no ViewManagers are
* available. The returned names should be unique within this package
*/
public fun getViewManagerNames(reactContext: ReactApplicationContext): Collection<String>
/**
* Creates and returns a ViewManager instance for the specified name.
*
* This method is called lazily when a ViewManager is actually needed by the JavaScript code,
* rather than during package initialization. The implementation should create and configure the
* appropriate ViewManager based on the provided name. The name parameter corresponds to one of
* the names returned by [getViewManagerNames].
*
* Implementations have flexibility in how they interpret the name and create ViewManagers. For
* example, they might use a factory pattern, reflection, or a simple name-to-class mapping.
*
* This method may be called on any thread, so implementations should ensure thread safety if
* necessary.
*
* @param reactContext The React application context, which provides access to the Android
* application context and React Native lifecycle information needed to initialize the
* ViewManager
* @param viewManagerName The name of the ViewManager to create, matching one of the names
* returned by [getViewManagerNames]
* @return A ViewManager instance for the specified name, or null if the name is not recognized or
* the ViewManager cannot be created. Returning null will result in a JavaScript error when the
* native component is used
*/
public fun createViewManager(
reactContext: ReactApplicationContext,
viewManagerName: String,
): ViewManager<in Nothing, in Nothing>?
}

View File

@@ -0,0 +1,54 @@
/*
* 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.animated
import com.facebook.react.bridge.JSApplicationCausedNativeException
import com.facebook.react.bridge.ReadableMap
/**
* Animated node that plays a role of value aggregator. It takes two or more value nodes as an input
* and outputs a sum of values outputted by those nodes.
*/
internal class AdditionAnimatedNode(
config: ReadableMap,
private val nativeAnimatedNodesManager: NativeAnimatedNodesManager,
) : ValueAnimatedNode() {
private val inputNodes: IntArray
init {
val input = config.getArray("input")
inputNodes =
if (input == null) {
IntArray(0)
} else {
IntArray(input.size()) { i -> input.getInt(i) }
}
}
override fun update() {
nodeValue = 0.0
nodeValue +=
inputNodes.fold(
0.0,
{ acc, id ->
val animatedNode = nativeAnimatedNodesManager.getNodeById(id)
if (animatedNode is ValueAnimatedNode) {
acc + animatedNode.getValue()
} else {
throw JSApplicationCausedNativeException(
"Illegal node ID set as an input for Animated.Add node"
)
}
},
)
}
override fun prettyPrint(): String =
"AdditionAnimatedNode[${tag}]: input nodes: ${inputNodes.joinToString()} - super: ${super.prettyPrint()}"
}

View File

@@ -0,0 +1,74 @@
/*
* 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.animated
import java.util.ArrayList
/** Base class for all Animated.js library node types that can be created on the "native" side. */
public abstract class AnimatedNode {
internal companion object {
internal const val INITIAL_BFS_COLOR: Int = 0
internal const val DEFAULT_ANIMATED_NODE_CHILD_COUNT: Int = 1
}
// TODO: T196787278 Reduce the visibility of these fields to package once we have
// converted the whole module to Kotlin
@JvmField
internal var children: MutableList<AnimatedNode>? =
null /* lazy-initialized when a child is added */
@JvmField internal var activeIncomingNodes: Int = 0
@JvmField internal var BFSColor: Int = INITIAL_BFS_COLOR
@JvmField internal var tag: Int = -1
internal fun addChild(child: AnimatedNode) {
val currentChildren =
children
?: ArrayList<AnimatedNode>(DEFAULT_ANIMATED_NODE_CHILD_COUNT).also { children = it }
currentChildren.add(child)
child.onAttachedToNode(this)
}
internal fun removeChild(child: AnimatedNode): Unit {
val currentChildren = children ?: return
child.onDetachedFromNode(this)
currentChildren.remove(child)
}
/**
* Subclasses may want to override this method in order to store a reference to the parent of a
* given node that can then be used to calculate current node's value in [update]. In that case it
* is important to also override [onDetachedFromNode] to clear that reference once current node
* gets detached.
*/
internal open fun onAttachedToNode(parent: AnimatedNode): Unit = Unit
/** See [onAttachedToNode] */
internal open fun onDetachedFromNode(parent: AnimatedNode): Unit = Unit
/**
* This method will be run on each node at most once every repetition of the animation loop. It
* will be executed on a node only when all the node's parent has already been updated. Therefore
* it can be used to calculate node's value.
*/
internal open fun update(): Unit = Unit
/**
* Pretty-printer for the AnimatedNode. Only called in production pre-crash for debug diagnostics.
*/
internal abstract fun prettyPrint(): String
internal fun prettyPrintWithChildren(): String {
val currentChildren = children?.joinToString(" ")
return prettyPrint() +
if (!currentChildren.isNullOrBlank()) " children: $currentChildren" else ""
}
}

View File

@@ -0,0 +1,13 @@
/*
* 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.animated
/** Interface used to listen to [ValueAnimatedNode] updates. */
public fun interface AnimatedNodeValueListener {
public fun onValueUpdate(value: Double, offset: Double)
}

View File

@@ -0,0 +1,15 @@
/*
* 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.animated
import com.facebook.react.bridge.ReadableMap
/** Indicates that AnimatedNode is able to receive native config updates. */
internal fun interface AnimatedNodeWithUpdateableConfig {
fun onUpdateConfig(config: ReadableMap?)
}

View File

@@ -0,0 +1,39 @@
/*
* 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.animated
import com.facebook.react.bridge.Callback
import com.facebook.react.bridge.JSApplicationCausedNativeException
import com.facebook.react.bridge.ReadableMap
/**
* Base class for different types of animation drivers. Can be used to implement simple time-based
* animations as well as spring based animations.
*/
internal abstract class AnimationDriver {
@JvmField internal var hasFinished = false
@JvmField internal var animatedValue: ValueAnimatedNode? = null
@JvmField internal var endCallback: Callback? = null
@JvmField internal var id = 0
/**
* This method gets called in the main animation loop with a frame time passed down from the
* android choreographer callback.
*/
abstract fun runAnimationStep(frameTimeNanos: Long)
/**
* This method will get called when some of the configuration gets updated while the animation is
* running. In that case animation should restart keeping its internal state to provide a smooth
* transition. E.g. in case of a spring animation we want to keep the current value and speed and
* start animating with the new properties (different destination or spring settings)
*/
open fun resetConfig(config: ReadableMap) {
throw JSApplicationCausedNativeException("Animation config for AnimationDriver cannot be reset")
}
}

View File

@@ -0,0 +1,111 @@
/*
* 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.animated
import android.content.Context
import android.graphics.Color
import com.facebook.react.bridge.ColorPropConverter
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.views.view.ColorUtil.normalize
/** Animated node that represents a color. */
internal class ColorAnimatedNode(
config: ReadableMap,
private val nativeAnimatedNodesManager: NativeAnimatedNodesManager,
private val reactApplicationContext: ReactApplicationContext,
) : AnimatedNode(), AnimatedNodeWithUpdateableConfig {
private var rNodeId = 0
private var gNodeId = 0
private var bNodeId = 0
private var aNodeId = 0
private var nativeColor: ReadableMap? = null
private var nativeColorApplied = false
val color: Int
get() {
tryApplyNativeColor()
val rNode = nativeAnimatedNodesManager.getNodeById(rNodeId) as ValueAnimatedNode?
val gNode = nativeAnimatedNodesManager.getNodeById(gNodeId) as ValueAnimatedNode?
val bNode = nativeAnimatedNodesManager.getNodeById(bNodeId) as ValueAnimatedNode?
val aNode = nativeAnimatedNodesManager.getNodeById(aNodeId) as ValueAnimatedNode?
val r = rNode?.nodeValue ?: 0.0
val g = gNode?.nodeValue ?: 0.0
val b = bNode?.nodeValue ?: 0.0
val a = aNode?.nodeValue ?: 0.0
return normalize(r, g, b, a)
}
private val context: Context?
get() {
// There are cases where the activity may not exist (such as for VRShell panel apps). In this
// case we will search for a view associated with a PropsAnimatedNode to get the context.
return reactApplicationContext.currentActivity ?: getContextHelper(this)
}
init {
onUpdateConfig(config)
}
override fun onUpdateConfig(config: ReadableMap?) {
if (config != null) {
rNodeId = config.getInt("r")
gNodeId = config.getInt("g")
bNodeId = config.getInt("b")
aNodeId = config.getInt("a")
nativeColor = config.getMap("nativeColor")
nativeColorApplied = false
tryApplyNativeColor()
} else {
rNodeId = 0
gNodeId = 0
bNodeId = 0
aNodeId = 0
nativeColor = null
nativeColorApplied = false
}
}
override fun prettyPrint(): String =
"ColorAnimatedNode[$tag]: r: $rNodeId g: $gNodeId b: $bNodeId a: $aNodeId"
private fun tryApplyNativeColor() {
if (nativeColor == null || nativeColorApplied) {
return
}
val context = context ?: return
val color = ColorPropConverter.getColor(nativeColor, context) ?: return
val rNode = nativeAnimatedNodesManager.getNodeById(rNodeId) as ValueAnimatedNode?
val gNode = nativeAnimatedNodesManager.getNodeById(gNodeId) as ValueAnimatedNode?
val bNode = nativeAnimatedNodesManager.getNodeById(bNodeId) as ValueAnimatedNode?
val aNode = nativeAnimatedNodesManager.getNodeById(aNodeId) as ValueAnimatedNode?
rNode?.nodeValue = Color.red(color).toDouble()
gNode?.nodeValue = Color.green(color).toDouble()
bNode?.nodeValue = Color.blue(color).toDouble()
aNode?.nodeValue = Color.alpha(color) / 255.0
nativeColorApplied = true
}
companion object {
private fun getContextHelper(node: AnimatedNode): Context? {
// Search children depth-first until we get to a PropsAnimatedNode, from which we can
// get the view and its context
node.children?.let { children ->
for (child in children) {
return if (child is PropsAnimatedNode) {
val view = child.connectedView
view?.context
} else {
getContextHelper(child)
}
}
}
return null
}
}
}

View File

@@ -0,0 +1,73 @@
/*
* 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.animated
import com.facebook.react.bridge.ReadableMap
import kotlin.math.abs
import kotlin.math.exp
/**
* Implementation of [AnimationDriver] providing support for decay animations. The implementation is
* copied from the JS version in `AnimatedImplementation.js`.
*/
internal class DecayAnimation(config: ReadableMap) : AnimationDriver() {
private var velocity: Double = 0.0
private var deceleration: Double = 0.0
private var startFrameTimeMillis: Long = -1
private var fromValue: Double = 0.0
private var lastValue: Double = 0.0
private var iterations: Int = 1
private var currentLoop: Int = 1
init {
resetConfig(config)
}
override fun resetConfig(config: ReadableMap) {
velocity = config.getDouble("velocity")
deceleration = config.getDouble("deceleration")
startFrameTimeMillis = -1
fromValue = 0.0
lastValue = 0.0
iterations = if (config.hasKey("iterations")) config.getInt("iterations") else 1
currentLoop = 1
hasFinished = iterations == 0
}
override fun runAnimationStep(frameTimeNanos: Long) {
val animatedValue = requireNotNull(animatedValue) { "Animated value should not be null" }
val frameTimeMillis = frameTimeNanos / 1000000
if (startFrameTimeMillis == -1L) {
// since this is the first animation step, consider the start to be on the previous frame
startFrameTimeMillis = frameTimeMillis - 16
if (fromValue == lastValue) { // first iteration, assign [fromValue] based on [animatedValue]
fromValue = animatedValue.nodeValue
} else { // not the first iteration, reset [animatedValue] based on [fromValue]
animatedValue.nodeValue = fromValue
}
lastValue = animatedValue.nodeValue
}
val value =
(fromValue +
velocity / (1 - deceleration) *
(1 - exp(-(1 - deceleration) * (frameTimeMillis - startFrameTimeMillis))))
if (abs(lastValue - value) < 0.1) {
if (iterations == -1 || currentLoop < iterations) { // looping animation, return to start
// set [startFrameTimeMillis] to -1 to reset instance variables on the next
// [runAnimationStep]
startFrameTimeMillis = -1
currentLoop++
} else { // animation has completed
hasFinished = true
return
}
}
lastValue = value
animatedValue.nodeValue = value
}
}

View File

@@ -0,0 +1,52 @@
/*
* 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.animated
import com.facebook.react.bridge.JSApplicationCausedNativeException
import com.facebook.react.bridge.ReadableMap
import kotlin.math.max
import kotlin.math.min
internal class DiffClampAnimatedNode(
config: ReadableMap,
private val nativeAnimatedNodesManager: NativeAnimatedNodesManager,
) : ValueAnimatedNode() {
private val inputNodeTag: Int
private val minValue: Double
private val maxValue: Double
private var lastValue: Double = 0.0
private val inputNodeValue: Double
get() {
val animatedNode = nativeAnimatedNodesManager.getNodeById(inputNodeTag)
if (animatedNode == null || animatedNode !is ValueAnimatedNode) {
throw JSApplicationCausedNativeException(
"Illegal node ID set as an input for Animated.DiffClamp node"
)
}
return animatedNode.getValue()
}
init {
inputNodeTag = config.getInt("input")
minValue = config.getDouble("min")
maxValue = config.getDouble("max")
nodeValue = lastValue
}
override fun update() {
val value = inputNodeValue
val diff = value - lastValue
lastValue = value
nodeValue = min(max(nodeValue + diff, minValue), maxValue)
}
override fun prettyPrint(): String =
"DiffClampAnimatedNode[$tag]: InputNodeTag: $inputNodeTag min: $minValue " +
"max: $maxValue lastValue: $lastValue super: ${super.prettyPrint()}"
}

View File

@@ -0,0 +1,57 @@
/*
* 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.animated
import com.facebook.react.bridge.JSApplicationCausedNativeException
import com.facebook.react.bridge.ReadableMap
/**
* Animated node which takes two or more value node as an input and outputs an in-order division of
* their values.
*/
internal class DivisionAnimatedNode(
config: ReadableMap,
private val nativeAnimatedNodesManager: NativeAnimatedNodesManager,
) : ValueAnimatedNode() {
private val inputNodes: IntArray
init {
val input = config.getArray("input")
inputNodes =
if (input == null) {
IntArray(0)
} else {
IntArray(input.size()) { i -> input.getInt(i) }
}
}
override fun update() {
inputNodes.forEachIndexed { i, inputNode ->
val animatedNode = nativeAnimatedNodesManager.getNodeById(inputNode)
if (animatedNode != null && animatedNode is ValueAnimatedNode) {
val v = animatedNode.nodeValue
if (i == 0) {
nodeValue = v
} else if (v == 0.0) {
throw JSApplicationCausedNativeException(
"Detected a division by zero in Animated.divide node with Animated ID $tag"
)
} else {
nodeValue /= v
}
} else {
throw JSApplicationCausedNativeException(
"Illegal node ID set as an input for Animated.divide node with Animated ID $tag"
)
}
}
}
override fun prettyPrint(): String =
"DivisionAnimatedNode[$tag]: input nodes: $inputNodes - super: ${super.prettyPrint()}"
}

View File

@@ -0,0 +1,74 @@
/*
* 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.animated
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.bridge.ReadableType
import com.facebook.react.bridge.UnexpectedNativeTypeException
import com.facebook.react.bridge.WritableMap
import com.facebook.react.uimanager.events.EventCategoryDef
import com.facebook.react.uimanager.events.RCTModernEventEmitter
/** Handles updating a [ValueAnimatedNode] when an event gets dispatched. */
internal class EventAnimationDriver(
@JvmField var eventName: String,
@JvmField internal var viewTag: Int,
private val eventPath: List<String>,
@JvmField internal var valueNode: ValueAnimatedNode,
) : RCTModernEventEmitter {
override fun receiveEvent(
surfaceId: Int,
targetTag: Int,
eventName: String,
canCoalesceEvent: Boolean,
customCoalesceKey: Int,
params: WritableMap?,
@EventCategoryDef category: Int,
) {
requireNotNull(params) { "Native animated events must have event data." }
// Get the new value for the node by looking into the event map using the provided event path.
var currMap: ReadableMap? = params
var currArray: ReadableArray? = null
for (i in 0 until eventPath.size - 1) {
if (currMap != null) {
val key = eventPath[i]
val keyType = currMap.getType(key)
if (keyType == ReadableType.Map) {
currMap = currMap.getMap(key)
currArray = null
} else if (keyType == ReadableType.Array) {
currArray = currMap.getArray(key)
currMap = null
} else {
throw UnexpectedNativeTypeException("Unexpected type $keyType for key '$key'")
}
} else {
val index = eventPath[i].toInt()
val keyType = currArray?.getType(index)
if (keyType == ReadableType.Map) {
currMap = currArray.getMap(index)
currArray = null
} else if (keyType == ReadableType.Array) {
currArray = currArray.getArray(index)
currMap = null
} else {
throw UnexpectedNativeTypeException("Unexpected type $keyType for index '$index'")
}
}
}
val lastKey = eventPath[eventPath.size - 1]
if (currMap != null) {
valueNode.nodeValue = currMap.getDouble(lastKey)
} else {
val lastIndex = lastKey.toInt()
valueNode.nodeValue = currArray?.getDouble(lastIndex) ?: 0.0
}
}
}

View File

@@ -0,0 +1,104 @@
/*
* 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.animated
import com.facebook.common.logging.FLog
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.bridge.ReadableType
import com.facebook.react.common.ReactConstants
import com.facebook.react.common.build.ReactBuildConfig
/**
* Implementation of [AnimationDriver] which provides a support for simple time-based animations
* that are pre-calculate on the JS side. For each animation frame JS provides a value from 0 to 1
* that indicates a progress of the animation at that frame.
*/
internal class FrameBasedAnimationDriver(config: ReadableMap) : AnimationDriver() {
private var startFrameTimeNanos: Long = -1
private var frames: DoubleArray = DoubleArray(0)
private var toValue = 0.0
private var fromValue = 0.0
private var iterations = 1
private var currentLoop = 1
private var logCount = 0
init {
resetConfig(config)
}
override fun resetConfig(config: ReadableMap) {
val framesConfig = config.getArray("frames")
if (framesConfig != null) {
val numberOfFrames = framesConfig.size()
if (frames.size != numberOfFrames) {
frames = DoubleArray(numberOfFrames) { i -> framesConfig.getDouble(i) }
}
}
toValue =
if (config.hasKey("toValue") && config.getType("toValue") == ReadableType.Number)
config.getDouble("toValue")
else 0.0
iterations =
if (config.hasKey("iterations") && config.getType("iterations") == ReadableType.Number)
config.getInt("iterations")
else 1
currentLoop = 1
hasFinished = iterations == 0
startFrameTimeNanos = -1
}
override fun runAnimationStep(frameTimeNanos: Long) {
val animatedValue = requireNotNull(animatedValue) { "Animated value should not be null" }
if (startFrameTimeNanos < 0) {
startFrameTimeNanos = frameTimeNanos
if (currentLoop == 1) {
// initiate start value when animation runs for the first time
fromValue = animatedValue.nodeValue
}
}
val timeFromStartMillis = (frameTimeNanos - startFrameTimeNanos) / 1000000
val frameIndex = Math.round(timeFromStartMillis / FRAME_TIME_MILLIS).toInt()
if (frameIndex < 0) {
val message =
("Calculated frame index should never be lower than 0. Called with frameTimeNanos " +
frameTimeNanos +
" and mStartFrameTimeNanos " +
startFrameTimeNanos)
check(!ReactBuildConfig.DEBUG) { message }
if (logCount < 100) {
FLog.w(ReactConstants.TAG, message)
logCount++
}
return
} else if (hasFinished) {
// nothing to do here
return
}
val nextValue: Double
if (frameIndex >= frames.size - 1) {
if (iterations == -1 || currentLoop < iterations) {
// Use last frame value, just in case it's different from mToValue
nextValue = fromValue + frames[frames.size - 1] * (toValue - fromValue)
startFrameTimeNanos = -1
currentLoop++
} else {
// animation has completed, no more frames left
nextValue = toValue
hasFinished = true
}
} else {
nextValue = fromValue + frames[frameIndex] * (toValue - fromValue)
}
animatedValue.nodeValue = nextValue
}
companion object {
// 60FPS
private const val FRAME_TIME_MILLIS = 1000.0 / 60.0
}
}

View File

@@ -0,0 +1,292 @@
/*
* 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.animated
import androidx.core.graphics.ColorUtils
import com.facebook.common.logging.FLog
import com.facebook.react.bridge.JSApplicationIllegalArgumentException
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.bridge.ReadableType
import java.util.regex.Pattern
/**
* Animated node that corresponds to `AnimatedInterpolation` from AnimatedImplementation.js.
*
* Currently only a linear interpolation is supported on an input range of an arbitrary size.
*/
internal class InterpolationAnimatedNode(config: ReadableMap) : ValueAnimatedNode() {
private enum class OutputType {
Number,
Color,
String,
}
private val inputRange: DoubleArray = fromDoubleArray(config.getArray("inputRange"))
private var outputRange: Any? = null
private var outputType: OutputType? = null
private var pattern: String? = null
private val extrapolateLeft: String? = config.getString("extrapolateLeft")
private val extrapolateRight: String? = config.getString("extrapolateRight")
private var parent: ValueAnimatedNode? = null
private var objectValue: Any? = null
init {
val output = config.getArray("outputRange")
if (COLOR_OUTPUT_TYPE == config.getString("outputType")) {
outputType = OutputType.Color
outputRange = fromIntArray(output)
} else if (output?.getType(0) == ReadableType.String) {
outputType = OutputType.String
outputRange = fromStringPattern(output)
pattern = output.getString(0)
} else if (output != null && output.size() > 0 && output.getType(0) != ReadableType.Number) {
FLog.e(
TAG,
"Unsupported value type in interpolation outputRange: expected Number but got " +
"${output.getType(0)}. This may indicate PlatformColor or other unsupported " +
"values are being used. Interpolation will not work correctly.",
)
outputType = OutputType.Number
outputRange = DoubleArray(output.size())
} else {
outputType = OutputType.Number
outputRange = fromDoubleArray(output)
}
}
override fun onAttachedToNode(parent: AnimatedNode) {
check(this.parent == null) { "Parent already attached" }
require(parent is ValueAnimatedNode) { "Parent is of an invalid type" }
this.parent = parent
}
override fun onDetachedFromNode(parent: AnimatedNode) {
require(parent === this.parent) { "Invalid parent node provided" }
this.parent = null
}
override fun update() {
// If the graph is in the middle of being created, just skip this unattached node.
val parentValue = parent?.getValue() ?: return
when (outputType) {
OutputType.Number ->
nodeValue =
interpolate(
parentValue,
inputRange,
outputRange as DoubleArray,
extrapolateLeft,
extrapolateRight,
)
OutputType.Color ->
objectValue =
Integer.valueOf(interpolateColor(parentValue, inputRange, outputRange as IntArray))
OutputType.String ->
pattern?.let {
@Suppress("UNCHECKED_CAST")
objectValue =
interpolateString(
it,
parentValue,
inputRange,
outputRange as Array<DoubleArray>,
extrapolateLeft,
extrapolateRight,
)
}
else -> {}
}
}
override fun getAnimatedObject(): Any? = objectValue
override fun prettyPrint(): String =
"InterpolationAnimatedNode[$tag] super: ${super.prettyPrint()}"
companion object {
private const val TAG = "InterpolationAnimatedNode"
const val EXTRAPOLATE_TYPE_IDENTITY: String = "identity"
const val EXTRAPOLATE_TYPE_CLAMP: String = "clamp"
const val EXTRAPOLATE_TYPE_EXTEND: String = "extend"
private val numericPattern: Pattern =
Pattern.compile("[+-]?(\\d+\\.?\\d*|\\.\\d+)([eE][+-]?\\d+)?")
private const val COLOR_OUTPUT_TYPE: String = "color"
private fun fromDoubleArray(array: ReadableArray?): DoubleArray {
val size = array?.size() ?: return DoubleArray(0)
val res = DoubleArray(size)
for (i in res.indices) {
res[i] = array.getDouble(i)
}
return res
}
private fun fromIntArray(array: ReadableArray?): IntArray {
val size = array?.size() ?: return IntArray(0)
val res = IntArray(size)
for (i in res.indices) {
res[i] = array.getInt(i)
}
return res
}
private fun fromStringPattern(array: ReadableArray): Array<DoubleArray?> {
val size = array.size()
val outputRange = arrayOfNulls<DoubleArray>(size)
// Match the first pattern into a List, since we don't know its length yet
var m = numericPattern.matcher(array.getString(0).orEmpty())
val firstOutputRange: MutableList<Double> = ArrayList()
while (m.find()) {
firstOutputRange.add(m.group().toDouble())
}
val firstOutputRangeArr = DoubleArray(firstOutputRange.size)
for (i in firstOutputRange.indices) {
firstOutputRangeArr[i] = firstOutputRange[i]
}
outputRange[0] = firstOutputRangeArr
for (i in 1 until size) {
val outputArr = DoubleArray(firstOutputRangeArr.size)
var j = 0
m = numericPattern.matcher(array.getString(i).orEmpty())
while (m.find() && j < firstOutputRangeArr.size) {
outputArr[j++] = m.group().toDouble()
}
outputRange[i] = outputArr
}
return outputRange
}
fun interpolate(
value: Double,
inputMin: Double,
inputMax: Double,
outputMin: Double,
outputMax: Double,
extrapolateLeft: String?,
extrapolateRight: String?,
): Double {
var result = value
// Extrapolate
if (result < inputMin) {
when (extrapolateLeft) {
EXTRAPOLATE_TYPE_IDENTITY -> return result
EXTRAPOLATE_TYPE_CLAMP -> result = inputMin
EXTRAPOLATE_TYPE_EXTEND -> {}
else ->
throw JSApplicationIllegalArgumentException(
"Invalid extrapolation type " + extrapolateLeft + "for left extrapolation"
)
}
}
if (result > inputMax) {
when (extrapolateRight) {
EXTRAPOLATE_TYPE_IDENTITY -> return result
EXTRAPOLATE_TYPE_CLAMP -> result = inputMax
EXTRAPOLATE_TYPE_EXTEND -> {}
else ->
throw JSApplicationIllegalArgumentException(
"Invalid extrapolation type " + extrapolateRight + "for right extrapolation"
)
}
}
if (outputMin == outputMax) {
return outputMin
}
return if (inputMin == inputMax) {
if (value <= inputMin) {
outputMin
} else outputMax
} else outputMin + (outputMax - outputMin) * (result - inputMin) / (inputMax - inputMin)
}
fun interpolate(
value: Double,
inputRange: DoubleArray,
outputRange: DoubleArray,
extrapolateLeft: String?,
extrapolateRight: String?,
): Double {
val rangeIndex = findRangeIndex(value, inputRange)
return interpolate(
value,
inputRange[rangeIndex],
inputRange[rangeIndex + 1],
outputRange[rangeIndex],
outputRange[rangeIndex + 1],
extrapolateLeft,
extrapolateRight,
)
}
fun interpolateColor(value: Double, inputRange: DoubleArray, outputRange: IntArray): Int {
val rangeIndex = findRangeIndex(value, inputRange)
val outputMin = outputRange[rangeIndex]
val outputMax = outputRange[rangeIndex + 1]
if (outputMin == outputMax) {
return outputMin
}
val inputMin = inputRange[rangeIndex]
val inputMax = inputRange[rangeIndex + 1]
if (inputMin == inputMax) {
return if (value <= inputMin) {
outputMin
} else outputMax
}
val ratio = (value - inputMin) / (inputMax - inputMin)
return ColorUtils.blendARGB(outputMin, outputMax, ratio.toFloat())
}
fun interpolateString(
pattern: String,
value: Double,
inputRange: DoubleArray,
outputRange: Array<DoubleArray>,
extrapolateLeft: String?,
extrapolateRight: String?,
): String {
val rangeIndex = findRangeIndex(value, inputRange)
val sb = StringBuffer(pattern.length)
val m = numericPattern.matcher(pattern)
var i = 0
while (m.find() && i < outputRange[rangeIndex].size) {
val v =
interpolate(
value,
inputRange[rangeIndex],
inputRange[rangeIndex + 1],
outputRange[rangeIndex][i],
outputRange[rangeIndex + 1][i],
extrapolateLeft,
extrapolateRight,
)
val intVal = v.toInt()
m.appendReplacement(sb, if (intVal.toDouble() != v) v.toString() else intVal.toString())
i++
}
m.appendTail(sb)
return sb.toString()
}
private fun findRangeIndex(value: Double, ranges: DoubleArray): Int {
var index = 1
while (index < ranges.size - 1) {
if (ranges[index] >= value) {
break
}
index++
}
return index - 1
}
}
}

View File

@@ -0,0 +1,36 @@
/*
* 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.animated
import com.facebook.react.bridge.JSApplicationCausedNativeException
import com.facebook.react.bridge.ReadableMap
internal class ModulusAnimatedNode(
config: ReadableMap,
private val nativeAnimatedNodesManager: NativeAnimatedNodesManager,
) : ValueAnimatedNode() {
private val inputNode: Int = config.getInt("input")
private val modulus: Double = config.getDouble("modulus")
override fun update() {
val animatedNode = nativeAnimatedNodesManager.getNodeById(inputNode)
if (animatedNode is ValueAnimatedNode) {
val animatedNodeValue = animatedNode.getValue()
nodeValue = (animatedNodeValue % modulus + modulus) % modulus
} else {
throw JSApplicationCausedNativeException(
"Illegal node ID set as an input for Animated.modulus node"
)
}
}
override fun prettyPrint(): String {
return "NativeAnimatedNodesManager[$tag] inputNode: $inputNode modulus: $modulus super: ${super.prettyPrint()}"
}
}

View File

@@ -0,0 +1,51 @@
/*
* 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.animated
import com.facebook.react.bridge.JSApplicationCausedNativeException
import com.facebook.react.bridge.ReadableMap
/**
* Animated node which takes two or more value node as an input and outputs a product of their
* values
*/
internal class MultiplicationAnimatedNode(
config: ReadableMap,
private val nativeAnimatedNodesManager: NativeAnimatedNodesManager,
) : ValueAnimatedNode() {
private var inputNodes: IntArray
init {
val input = config.getArray("input")
inputNodes =
if (input == null) {
IntArray(0)
} else {
IntArray(input.size()) { i -> input.getInt(i) }
}
}
override fun update() {
nodeValue = 1.0
for (i in inputNodes.indices) {
val animatedNode = nativeAnimatedNodesManager.getNodeById(inputNodes[i])
val multiplier =
if (animatedNode != null && animatedNode is ValueAnimatedNode) {
animatedNode.getValue()
} else {
throw JSApplicationCausedNativeException(
"Illegal node ID set as an input for Animated.multiply node"
)
}
nodeValue *= multiplier
}
}
override fun prettyPrint(): String =
"MultiplicationAnimatedNode[$tag]: input nodes: $inputNodes - super: ${super.prettyPrint()}"
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,803 @@
/*
* 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.animated
import android.util.SparseArray
import androidx.annotation.UiThread
import com.facebook.common.logging.FLog
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.Callback
import com.facebook.react.bridge.JSApplicationCausedNativeException
import com.facebook.react.bridge.JSApplicationIllegalArgumentException
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactNoCrashSoftException
import com.facebook.react.bridge.ReactSoftExceptionLogger
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.bridge.UiThreadUtil
import com.facebook.react.bridge.WritableArray
import com.facebook.react.bridge.buildReadableMap
import com.facebook.react.uimanager.UIManagerHelper
import com.facebook.react.uimanager.common.UIManagerType
import com.facebook.react.uimanager.events.Event
import com.facebook.react.uimanager.events.EventDispatcherListener
import java.util.ArrayDeque
import java.util.ArrayList
import java.util.HashSet
import java.util.LinkedList
import java.util.Queue
/**
* This is the main class that coordinates how native animated JS implementation drives UI changes.
*
* It implements a management interface for animated nodes graph as well as implements a graph
* traversal algorithm that is run for each animation frame.
*
* For each animation frame we visit animated nodes that might've been updated as well as their
* children that may use parent's values to update themselves. At the end of the traversal algorithm
* we expect to reach a special type of the node: PropsAnimatedNode that is then responsible for
* calculating property map which can be sent to native view hierarchy to update the view.
*
* IMPORTANT: This class should be accessed only from the UI Thread
*/
public class NativeAnimatedNodesManager(
private val reactApplicationContext: ReactApplicationContext?
) : EventDispatcherListener {
private val animatedNodes = SparseArray<AnimatedNode>()
private val activeAnimations = SparseArray<AnimationDriver>()
private val updatedNodes = SparseArray<AnimatedNode>()
// List of event animation drivers for an event on view.
// There may be multiple drivers for the same event and view.
private val eventDrivers: MutableList<EventAnimationDriver> = ArrayList()
private var animatedGraphBFSColor = 0
// Used to avoid allocating a new array on every frame in `runUpdates` and `onEventDispatch`.
private val runUpdateNodeList: MutableList<AnimatedNode> = LinkedList()
private var eventListenerInitializedForFabric = false
private var eventListenerInitializedForNonFabric = false
private var warnedAboutGraphTraversal = false
/**
* Initialize event listeners for Fabric UIManager or non-Fabric UIManager, exactly once. Once
* Fabric is the only UIManager, this logic can be simplified. This is expected to only be called
* from the native module thread.
*
* @param uiManagerType
*/
public fun initializeEventListenerForUIManagerType(@UIManagerType uiManagerType: Int) {
val isEventListenerInitialized =
when (uiManagerType) {
UIManagerType.FABRIC -> eventListenerInitializedForFabric
else -> eventListenerInitializedForNonFabric
}
if (isEventListenerInitialized) {
return
}
val uiManager =
UIManagerHelper.getUIManager(checkNotNull(reactApplicationContext), uiManagerType)
if (uiManager != null) {
uiManager.eventDispatcher.addListener(this)
if (uiManagerType == UIManagerType.FABRIC) {
eventListenerInitializedForFabric = true
} else {
eventListenerInitializedForNonFabric = true
}
}
}
public fun getNodeById(id: Int): AnimatedNode? = animatedNodes.get(id)
public fun hasActiveAnimations(): Boolean = activeAnimations.size() > 0 || updatedNodes.size() > 0
@UiThread
public fun createAnimatedNode(tag: Int, config: ReadableMap) {
if (animatedNodes.get(tag) != null) {
throw JSApplicationIllegalArgumentException(
"createAnimatedNode: Animated node [$tag] already exists"
)
}
val node =
when (val type = config.getString("type")) {
"style" -> StyleAnimatedNode(config, this)
"value" -> ValueAnimatedNode(config)
"color" -> ColorAnimatedNode(config, this, checkNotNull(reactApplicationContext))
"props" -> PropsAnimatedNode(config, this)
"interpolation" -> InterpolationAnimatedNode(config)
"addition" -> AdditionAnimatedNode(config, this)
"subtraction" -> SubtractionAnimatedNode(config, this)
"division" -> DivisionAnimatedNode(config, this)
"multiplication" -> MultiplicationAnimatedNode(config, this)
"modulus" -> ModulusAnimatedNode(config, this)
"diffclamp" -> DiffClampAnimatedNode(config, this)
"transform" -> TransformAnimatedNode(config, this)
"tracking" -> TrackingAnimatedNode(config, this)
"object" -> ObjectAnimatedNode(config, this)
else -> throw JSApplicationIllegalArgumentException("Unsupported node type: $type")
}
node.tag = tag
animatedNodes.put(tag, node)
updatedNodes.put(tag, node)
}
@UiThread
public fun updateAnimatedNodeConfig(tag: Int, config: ReadableMap?) {
val node =
animatedNodes.get(tag)
?: throw JSApplicationIllegalArgumentException(
"updateAnimatedNode: Animated node [$tag] does not exist"
)
if (node is AnimatedNodeWithUpdateableConfig) {
stopAnimationsForNode(node)
(node as AnimatedNodeWithUpdateableConfig).onUpdateConfig(config)
updatedNodes.put(tag, node)
}
}
@UiThread
public fun dropAnimatedNode(tag: Int) {
animatedNodes.remove(tag)
updatedNodes.remove(tag)
}
@UiThread
public fun startListeningToAnimatedNodeValue(tag: Int, listener: AnimatedNodeValueListener?) {
val node = animatedNodes[tag]
if (node == null || node !is ValueAnimatedNode) {
throw JSApplicationIllegalArgumentException(
("startListeningToAnimatedNodeValue: Animated node [${tag}] does not exist, or is not a 'value' node")
)
}
node.setValueListener(listener)
}
@UiThread
public fun stopListeningToAnimatedNodeValue(tag: Int) {
val node = animatedNodes.get(tag)
if (node == null || node !is ValueAnimatedNode) {
throw JSApplicationIllegalArgumentException(
("startListeningToAnimatedNodeValue: Animated node [${tag}] does not exist, or is not a 'value' node")
)
}
node.setValueListener(null)
}
@UiThread
public fun setAnimatedNodeValue(tag: Int, value: Double) {
val node = animatedNodes.get(tag)
if (node == null || node !is ValueAnimatedNode) {
throw JSApplicationIllegalArgumentException(
("setAnimatedNodeValue: Animated node [${tag}] does not exist, or is not a 'value' node")
)
}
stopAnimationsForNode(node)
node.nodeValue = value
updatedNodes.put(tag, node)
}
@UiThread
public fun setAnimatedNodeOffset(tag: Int, offset: Double) {
val node = animatedNodes.get(tag)
if (node == null || node !is ValueAnimatedNode) {
throw JSApplicationIllegalArgumentException(
("setAnimatedNodeOffset: Animated node [${tag}] does not exist, or is not a 'value' node")
)
}
node.offset = offset
updatedNodes.put(tag, node)
}
@UiThread
public fun flattenAnimatedNodeOffset(tag: Int) {
val node = animatedNodes.get(tag)
if (node == null || node !is ValueAnimatedNode) {
throw JSApplicationIllegalArgumentException(
("flattenAnimatedNodeOffset: Animated node [${tag}] does not exist, or is not a 'value' node")
)
}
node.flattenOffset()
}
@UiThread
public fun extractAnimatedNodeOffset(tag: Int) {
val node = animatedNodes.get(tag)
if (node == null || node !is ValueAnimatedNode) {
throw JSApplicationIllegalArgumentException(
("extractAnimatedNodeOffset: Animated node [${tag}] does not exist, or is not a 'value' node")
)
}
node.extractOffset()
}
@UiThread
public fun startAnimatingNode(
animationId: Int,
animatedNodeTag: Int,
animationConfig: ReadableMap,
endCallback: Callback?,
) {
val node =
animatedNodes.get(animatedNodeTag)
?: throw JSApplicationIllegalArgumentException(
"startAnimatingNode: Animated node [$animatedNodeTag] does not exist"
)
if (node !is ValueAnimatedNode) {
throw JSApplicationIllegalArgumentException(
("startAnimatingNode: Animated node [${animatedNodeTag}] should be of type ValueAnimatedNode")
)
}
val existingDriver = activeAnimations[animationId]
if (existingDriver != null) {
// animation with the given ID is already running, we need to update its configuration instead
// of spawning a new one
existingDriver.resetConfig(animationConfig)
return
}
val animation =
when (val type = animationConfig.getString("type")) {
"frames" -> FrameBasedAnimationDriver(animationConfig)
"spring" -> SpringAnimation(animationConfig)
"decay" -> DecayAnimation(animationConfig)
else -> {
throw JSApplicationIllegalArgumentException(
"startAnimatingNode: Unsupported animation type [$animatedNodeTag]: $type"
)
}
}
animation.id = animationId
animation.endCallback = endCallback
animation.animatedValue = node
activeAnimations.put(animationId, animation)
}
@UiThread
private fun stopAnimationsForNode(animatedNode: AnimatedNode) {
// in most of the cases there should never be more than a few active animations running at the
// same time. Therefore it does not make much sense to create an animationId -> animation
// object map that would require additional memory just to support the use-case of stopping
// an animation
var events: WritableArray? = null
var i = 0
while (i < activeAnimations.size()) {
val animation = activeAnimations.valueAt(i)
if (animatedNode == animation.animatedValue) {
val animatedValueNonnull = checkNotNull(animation.animatedValue)
if (animation.endCallback != null) {
// Invoke animation end callback with {finished: false}
val endCallbackResponse = buildReadableMap {
put("finished", false)
put("value", animatedValueNonnull.nodeValue)
put("offset", animatedValueNonnull.offset)
}
animation.endCallback?.invoke(endCallbackResponse)
} else if (reactApplicationContext != null) {
// If no callback is passed in, this /may/ be an animation set up by the single-op
// instruction from JS, meaning that no jsi::functions are passed into native and
// we communicate via RCTDeviceEventEmitter instead of callbacks.
val params = buildReadableMap {
put("animationId", animation.id)
put("finished", false)
put("value", animatedValueNonnull.nodeValue)
put("offset", animatedValueNonnull.offset)
}
events = events ?: Arguments.createArray()
events.pushMap(params)
}
activeAnimations.removeAt(i)
i--
}
i++
}
if (events != null) {
reactApplicationContext?.emitDeviceEvent("onNativeAnimatedModuleAnimationFinished", events)
}
}
@UiThread
public fun stopAnimation(animationId: Int) {
// in most of the cases there should never be more than a few active animations running at the
// same time. Therefore it does not make much sense to create an animationId -> animation
// object map that would require additional memory just to support the use-case of stopping
// an animation
var events: WritableArray? = null
for (i in 0..<activeAnimations.size()) {
val animation = activeAnimations.valueAt(i)
if (animation.id == animationId) {
if (animation.endCallback != null) {
// Invoke animation end callback with {finished: false}
val endCallbackResponse = buildReadableMap {
put("finished", false)
put("value", checkNotNull(animation.animatedValue).nodeValue)
put("offset", checkNotNull(animation.animatedValue).offset)
}
checkNotNull(animation.endCallback).invoke(endCallbackResponse)
} else if (reactApplicationContext != null) {
// If no callback is passed in, this /may/ be an animation set up by the single-op
// instruction from JS, meaning that no jsi::functions are passed into native and
// we communicate via RCTDeviceEventEmitter instead of callbacks.
val params = buildReadableMap {
put("animationId", animation.id)
put("finished", false)
put("value", checkNotNull(animation.animatedValue).nodeValue)
put("offset", checkNotNull(animation.animatedValue).offset)
}
events = events ?: Arguments.createArray()
events.pushMap(params)
}
activeAnimations.removeAt(i)
break
}
}
if (events != null) {
reactApplicationContext?.emitDeviceEvent("onNativeAnimatedModuleAnimationFinished", events)
}
// Do not throw an error in the case animation could not be found. We only keep "active"
// animations in the registry and there is a chance that Animated.js will enqueue a
// stopAnimation call after the animation has ended or the call will reach native thread only
// when the animation is already over.
}
@UiThread
public fun connectAnimatedNodes(parentNodeTag: Int, childNodeTag: Int) {
val parentNode =
animatedNodes.get(parentNodeTag)
?: throw JSApplicationIllegalArgumentException(
("connectAnimatedNodes: Animated node with tag (parent) [${parentNodeTag}] does not exist")
)
val childNode =
animatedNodes.get(childNodeTag)
?: throw JSApplicationIllegalArgumentException(
("connectAnimatedNodes: Animated node with tag (child) [${childNodeTag}] does not exist")
)
parentNode.addChild(childNode)
updatedNodes.put(childNodeTag, childNode)
}
public fun disconnectAnimatedNodes(parentNodeTag: Int, childNodeTag: Int) {
val parentNode =
animatedNodes.get(parentNodeTag)
?: throw JSApplicationIllegalArgumentException(
("disconnectAnimatedNodes: Animated node with tag (parent) [${parentNodeTag}] does not exist")
)
val childNode =
animatedNodes.get(childNodeTag)
?: throw JSApplicationIllegalArgumentException(
("disconnectAnimatedNodes: Animated node with tag (child) [${childNodeTag}] does not exist")
)
parentNode.removeChild(childNode)
updatedNodes.put(childNodeTag, childNode)
}
@UiThread
public fun connectAnimatedNodeToView(animatedNodeTag: Int, viewTag: Int) {
val node =
animatedNodes.get(animatedNodeTag)
?: throw JSApplicationIllegalArgumentException(
("connectAnimatedNodeToView: Animated node with tag [${animatedNodeTag}] does not exist")
)
if (node !is PropsAnimatedNode) {
throw JSApplicationIllegalArgumentException(
("connectAnimatedNodeToView: Animated node connected to view [${viewTag}] should be of type PropsAnimatedNode")
)
}
checkNotNull(reactApplicationContext) {
("connectAnimatedNodeToView: Animated node could not be connected, no ReactApplicationContext: $viewTag")
}
val uiManager = UIManagerHelper.getUIManagerForReactTag(reactApplicationContext, viewTag)
if (uiManager == null) {
ReactSoftExceptionLogger.logSoftException(
TAG,
ReactNoCrashSoftException(
("connectAnimatedNodeToView: Animated node could not be connected to UIManager - uiManager disappeared for tag: $viewTag")
),
)
return
}
node.connectToView(viewTag, uiManager)
updatedNodes.put(animatedNodeTag, node)
}
@UiThread
public fun disconnectAnimatedNodeFromView(animatedNodeTag: Int, viewTag: Int) {
val node =
animatedNodes.get(animatedNodeTag)
?: throw JSApplicationIllegalArgumentException(
("disconnectAnimatedNodeFromView: Animated node with tag [${animatedNodeTag}] does not exist")
)
if (node !is PropsAnimatedNode) {
throw JSApplicationIllegalArgumentException(
("disconnectAnimatedNodeFromView: Animated node connected to view [${viewTag}] should be of type PropsAnimatedNode")
)
}
node.disconnectFromView(viewTag)
}
@UiThread
public fun getValue(tag: Int, callback: Callback?) {
val node = animatedNodes.get(tag)
if (node == null || node !is ValueAnimatedNode) {
throw JSApplicationIllegalArgumentException(
"getValue: Animated node with tag [$tag] does not exist or is not a 'value' node"
)
}
val value = node.getValue()
if (callback != null) {
callback.invoke(value)
return
}
// If there's no callback, that means that JS is using the single-operation mode, and not
// passing any callbacks into Java.
// See NativeAnimatedHelper.js for details.
// Instead, we use RCTDeviceEventEmitter to pass data back to JS and emulate callbacks.
if (reactApplicationContext == null) {
return
}
val params = buildReadableMap {
put("tag", tag)
put("value", value)
}
reactApplicationContext.emitDeviceEvent("onNativeAnimatedModuleGetValue", params)
}
@UiThread
public fun restoreDefaultValues(animatedNodeTag: Int) {
val node = animatedNodes.get(animatedNodeTag) ?: return
// Restoring default values needs to happen before UIManager operations so it is
// possible the node hasn't been created yet if it is being connected and
// disconnected in the same batch. In that case we don't need to restore
// default values since it will never actually update the view.
if (node !is PropsAnimatedNode) {
throw JSApplicationIllegalArgumentException(
"Animated node connected to view [?] should be of type PropsAnimatedNode"
)
}
node.restoreDefaultValues()
}
@UiThread
public fun addAnimatedEventToView(
viewTag: Int,
eventHandlerName: String,
eventMapping: ReadableMap,
) {
val nodeTag = eventMapping.getInt("animatedValueTag")
val node =
animatedNodes.get(nodeTag)
?: throw JSApplicationIllegalArgumentException(
"addAnimatedEventToView: Animated node with tag [$nodeTag] does not exist"
)
if (node !is ValueAnimatedNode) {
throw JSApplicationIllegalArgumentException(
("addAnimatedEventToView: Animated node on view [${viewTag}] connected to event handler (${eventHandlerName}) should be of type ValueAnimatedNode")
)
}
val path = checkNotNull(eventMapping.getArray("nativeEventPath"))
val pathList: MutableList<String> = ArrayList(path.size())
for (i in 0..<path.size()) {
pathList.add(checkNotNull(path.getString(i)))
}
val eventName = normalizeEventName(eventHandlerName)
val eventDriver = EventAnimationDriver(eventName, viewTag, pathList, node)
eventDrivers.add(eventDriver)
if (eventName == "topScroll") {
// Handle the custom topScrollEnded event sent by the ScrollViews when the user stops dragging
addAnimatedEventToView(viewTag, "topScrollEnded", eventMapping)
}
}
@UiThread
public fun removeAnimatedEventFromView(
viewTag: Int,
eventHandlerName: String,
animatedValueTag: Int,
) {
val eventName = normalizeEventName(eventHandlerName)
eventDrivers
.find { driver ->
eventName == driver.eventName &&
viewTag == driver.viewTag &&
animatedValueTag == driver.valueNode.tag
}
?.let { driver -> eventDrivers.remove(driver) }
if (eventName == "topScroll") {
// Handle the custom topScrollEnded event sent by the ScrollViews when the user stops dragging
removeAnimatedEventFromView(viewTag, "topScrollEnded", animatedValueTag)
}
}
override fun onEventDispatch(event: Event<*>) {
// Events can be dispatched from any thread so we have to make sure handleEvent is run from the
// UI thread.
if (UiThreadUtil.isOnUiThread()) {
handleEvent(event)
} else {
UiThreadUtil.runOnUiThread { handleEvent(event) }
}
}
@UiThread
private fun handleEvent(event: Event<*>) {
if (eventDrivers.isEmpty()) {
return
}
var foundAtLeastOneDriver = false
val matchSpec = event.eventAnimationDriverMatchSpec
for (driver in eventDrivers) {
if (matchSpec != null && matchSpec.match(driver.viewTag, driver.eventName)) {
foundAtLeastOneDriver = true
stopAnimationsForNode(driver.valueNode)
event.dispatchModern(driver)
runUpdateNodeList.add(driver.valueNode)
}
}
if (foundAtLeastOneDriver) {
updateNodes(runUpdateNodeList)
runUpdateNodeList.clear()
}
}
/**
* Animation loop performs two BFSes over the graph of animated nodes. We use incremented
* `mAnimatedGraphBFSColor` to mark nodes as visited in each of the BFSes which saves additional
* loops for clearing "visited" states.
*
* First BFS starts with nodes that are in `mUpdatedNodes` (that is, their value have been
* modified from JS in the last batch of JS operations) or directly attached to an active
* animation (hence linked to objects from `mActiveAnimations`). In that step we calculate an
* attribute `activeIncomingNodes`. The second BFS runs in topological order over the sub-graph of
* *active* nodes. This is done by adding node to the BFS queue only if all its "predecessors"
* have already been visited.
*/
@UiThread
public fun runUpdates(frameTimeNanos: Long) {
UiThreadUtil.assertOnUiThread()
var hasFinishedAnimations = false
for (i in 0..<updatedNodes.size()) {
val node = updatedNodes.valueAt(i)
runUpdateNodeList.add(node)
}
// Clean mUpdatedNodes queue
updatedNodes.clear()
for (i in 0..<activeAnimations.size()) {
val animation = activeAnimations.valueAt(i)
animation.runAnimationStep(frameTimeNanos)
animation.animatedValue?.let { valueNode -> runUpdateNodeList.add(valueNode) }
if (animation.hasFinished) {
hasFinishedAnimations = true
}
}
updateNodes(runUpdateNodeList)
runUpdateNodeList.clear()
// Cleanup finished animations. Iterate over the array of animations and override ones that has
// finished, then resize `mActiveAnimations`.
if (hasFinishedAnimations) {
var events: WritableArray? = null
for (i in activeAnimations.size() - 1 downTo 0) {
val animation = activeAnimations.valueAt(i)
if (animation.hasFinished) {
val animatedValueNonnull = checkNotNull(animation.animatedValue)
if (animation.endCallback != null) {
val endCallbackResponse = buildReadableMap {
put("finished", true)
put("value", animatedValueNonnull.nodeValue)
put("offset", animatedValueNonnull.offset)
}
animation.endCallback?.invoke(endCallbackResponse)
} else if (reactApplicationContext != null) {
// If no callback is passed in, this /may/ be an animation set up by the single-op
// instruction from JS, meaning that no jsi::functions are passed into native and
// we communicate via RCTDeviceEventEmitter instead of callbacks.
val params = buildReadableMap {
put("animationId", animation.id)
put("finished", true)
put("value", animatedValueNonnull.nodeValue)
put("offset", animatedValueNonnull.offset)
}
events = events ?: Arguments.createArray()
events.pushMap(params)
}
activeAnimations.removeAt(i)
}
}
if (events != null) {
reactApplicationContext?.emitDeviceEvent("onNativeAnimatedModuleAnimationFinished", events)
}
}
}
internal fun getTagsOfConnectedNodes(tag: Int, eventName: String): Set<Int> {
val tags: MutableSet<Int> = HashSet()
// Filter only relevant animation drivers
eventDrivers.forEach { driver ->
if (eventName == driver.eventName && tag == driver.viewTag) {
tags.add(driver.viewTag)
driver.valueNode.children?.forEach { node -> tags.add(node.tag) }
}
}
return tags
}
@UiThread
private fun updateNodes(nodes: List<AnimatedNode>) {
var activeNodesCount = 0
var updatedNodesCount = 0
// STEP 1.
// BFS over graph of nodes. Update `mIncomingNodes` attribute for each node during that BFS.
// Store number of visited nodes in `activeNodesCount`. We "execute" active animations as a part
// of this step.
animatedGraphBFSColor++ /* use new color */
if (animatedGraphBFSColor == AnimatedNode.INITIAL_BFS_COLOR) {
// value "0" is used as an initial color for a new node, using it in BFS may cause some nodes
// to be skipped.
animatedGraphBFSColor++
}
val nodesQueue: Queue<AnimatedNode> = ArrayDeque()
for (node in nodes) {
if (node.BFSColor != animatedGraphBFSColor) {
node.BFSColor = animatedGraphBFSColor
activeNodesCount++
nodesQueue.add(node)
}
}
while (nodesQueue.isNotEmpty()) {
val nextNode = nodesQueue.poll()
nextNode?.children?.forEach { child ->
child.activeIncomingNodes++
if (child.BFSColor != animatedGraphBFSColor) {
child.BFSColor = animatedGraphBFSColor
activeNodesCount++
nodesQueue.add(child)
}
}
}
// STEP 2
// BFS over the graph of active nodes in topological order -> visit node only when all its
// "predecessors" in the graph have already been visited. It is important to visit nodes in that
// order as they may often use values of their predecessors in order to calculate "next state"
// of their own. We start by determining the starting set of nodes by looking for nodes with
// `activeIncomingNodes = 0` (those can only be the ones that we start BFS in the previous
// step). We store number of visited nodes in this step in `updatedNodesCount`
animatedGraphBFSColor++
if (animatedGraphBFSColor == AnimatedNode.INITIAL_BFS_COLOR) {
// see reasoning for this check a few lines above
animatedGraphBFSColor++
}
// find nodes with zero "incoming nodes", those can be either nodes from `mUpdatedNodes` or
// ones connected to active animations
for (node in nodes) {
if (node.activeIncomingNodes == 0 && node.BFSColor != animatedGraphBFSColor) {
node.BFSColor = animatedGraphBFSColor
updatedNodesCount++
nodesQueue.add(node)
}
}
// Run main "update" loop
var cyclesDetected = 0
while (!nodesQueue.isEmpty()) {
val nextNode = nodesQueue.poll()
try {
nextNode?.update()
if (nextNode is PropsAnimatedNode) {
// Send property updates to native view manager
nextNode.updateView()
}
} catch (e: JSApplicationCausedNativeException) {
// An exception is thrown if the view hasn't been created yet. This can happen because
// views are created in batches. If this particular view didn't make it into a batch yet,
// the view won't exist and an exception will be thrown when attempting to start an
// animation on it.
//
// Eat the exception rather than crashing. The impact is that we may drop one or more
// frames of the animation.
FLog.e(TAG, "Native animation workaround, frame lost as result of race condition", e)
}
if (nextNode is ValueAnimatedNode) {
// Potentially send events to JS when the node's value is updated
nextNode.onValueUpdate()
}
nextNode?.children?.forEach { child ->
child.activeIncomingNodes--
if (child.BFSColor != animatedGraphBFSColor && child.activeIncomingNodes == 0) {
child.BFSColor = animatedGraphBFSColor
updatedNodesCount++
nodesQueue.add(child)
} else if (child.BFSColor == animatedGraphBFSColor) {
cyclesDetected++
}
}
}
// Verify that we've visited *all* active nodes. Throw otherwise as this could mean there is a
// cycle in animated node graph, or that the graph is only partially set up. We also take
// advantage of the fact that all active nodes are visited in the step above so that all the
// nodes properties `activeIncomingNodes` are set to zero.
// In Fabric there can be race conditions between the JS thread setting up or tearing down
// animated nodes, and Fabric executing them on the UI thread, leading to temporary inconsistent
// states.
if (activeNodesCount != updatedNodesCount) {
if (warnedAboutGraphTraversal) {
return
}
warnedAboutGraphTraversal = true
// Before crashing or logging soft exception, log details about current graph setup
FLog.e(TAG, "Detected animation cycle or disconnected graph. ")
for (node in nodes) {
FLog.e(TAG, node.prettyPrintWithChildren())
}
// If we're running only in non-Fabric, we still throw an exception.
// In Fabric, it seems that animations enter an inconsistent state fairly often.
// We detect if the inconsistency is due to a cycle (a fatal error for which we must crash)
// or disconnected regions, indicating a partially-set-up animation graph, which is not
// fatal and can stay a warning.
val reason = if (cyclesDetected > 0) ("cycles ($cyclesDetected)") else "disconnected regions"
val ex =
IllegalStateException(
("Looks like animated nodes graph has ${reason}, there are $activeNodesCount but toposort visited only $updatedNodesCount")
)
if (eventListenerInitializedForFabric && cyclesDetected == 0) {
// TODO T71377544: investigate these SoftExceptions and see if we can remove entirely
// or fix the root cause
ReactSoftExceptionLogger.logSoftException(TAG, ReactNoCrashSoftException(ex))
} else if (eventListenerInitializedForFabric) {
// TODO T71377544: investigate these SoftExceptions and see if we can remove entirely
// or fix the root cause
ReactSoftExceptionLogger.logSoftException(TAG, ReactNoCrashSoftException(ex))
} else {
throw ex
}
} else {
warnedAboutGraphTraversal = false
}
}
private fun normalizeEventName(eventHandlerName: String): String =
// Fabric UIManager also makes this assumption
if (eventHandlerName.startsWith("on")) {
"top${eventHandlerName.substring(2)}"
} else {
eventHandlerName
}
private companion object {
private const val TAG = "NativeAnimatedNodesManager"
}
}

View File

@@ -0,0 +1,126 @@
/*
* 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.animated
import com.facebook.react.bridge.JavaOnlyArray
import com.facebook.react.bridge.JavaOnlyMap
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.bridge.ReadableType
/**
* Native counterpart of object animated node (see AnimatedObject class in
* AnimatedImplementation.js)
*/
internal class ObjectAnimatedNode(
config: ReadableMap,
private val nativeAnimatedNodesManager: NativeAnimatedNodesManager,
) : AnimatedNode() {
private val configClone: JavaOnlyMap = JavaOnlyMap.deepClone(config)
fun collectViewUpdates(propKey: String, propsMap: JavaOnlyMap) {
val valueType = configClone.getType(VALUE_KEY)
if (valueType == ReadableType.Map) {
propsMap.putMap(propKey, collectViewUpdatesHelper(configClone.getMap(VALUE_KEY)))
} else if (valueType == ReadableType.Array) {
propsMap.putArray(propKey, collectViewUpdatesHelper(configClone.getArray(VALUE_KEY)))
} else {
throw IllegalArgumentException("Invalid value type for ObjectAnimatedNode")
}
}
private fun collectViewUpdatesHelper(source: ReadableArray?): JavaOnlyArray? {
source ?: return null
val result = JavaOnlyArray()
for (i in 0 until source.size()) {
when (source.getType(i)) {
ReadableType.Null -> result.pushNull()
ReadableType.Boolean -> result.pushBoolean(source.getBoolean(i))
ReadableType.Number -> result.pushDouble(source.getDouble(i))
ReadableType.String -> result.pushString(source.getString(i))
ReadableType.Map -> {
val map = source.getMap(i)
if (
map != null &&
map.hasKey(NODE_TAG_KEY) &&
map.getType(NODE_TAG_KEY) == ReadableType.Number
) {
val node = nativeAnimatedNodesManager.getNodeById(map.getInt(NODE_TAG_KEY))
requireNotNull(node) { "Mapped value node does not exist" }
if (node is ValueAnimatedNode) {
val animatedObject = node.getAnimatedObject()
if (animatedObject is Int) {
result.pushInt(animatedObject)
} else if (animatedObject is String) {
result.pushString(animatedObject)
} else {
result.pushDouble(node.getValue())
}
} else if (node is ColorAnimatedNode) {
result.pushInt(node.color)
}
} else {
result.pushMap(collectViewUpdatesHelper(source.getMap(i)))
}
}
ReadableType.Array -> result.pushArray(collectViewUpdatesHelper(source.getArray(i)))
}
}
return result
}
private fun collectViewUpdatesHelper(source: ReadableMap?): JavaOnlyMap? {
source ?: return null
val result = JavaOnlyMap()
val iter = source.keySetIterator()
while (iter.hasNextKey()) {
val propKey = iter.nextKey()
when (source.getType(propKey)) {
ReadableType.Null -> result.putNull(propKey)
ReadableType.Boolean -> result.putBoolean(propKey, source.getBoolean(propKey))
ReadableType.Number -> result.putDouble(propKey, source.getDouble(propKey))
ReadableType.String -> result.putString(propKey, source.getString(propKey))
ReadableType.Map -> {
val map = source.getMap(propKey)
if (
map != null &&
map.hasKey(NODE_TAG_KEY) &&
map.getType(NODE_TAG_KEY) == ReadableType.Number
) {
val node = nativeAnimatedNodesManager.getNodeById(map.getInt(NODE_TAG_KEY))
requireNotNull(node) { "Mapped value node does not exist" }
if (node is ValueAnimatedNode) {
val animatedObject = node.getAnimatedObject()
if (animatedObject is Int) {
result.putInt(propKey, animatedObject)
} else if (animatedObject is String) {
result.putString(propKey, animatedObject)
} else {
result.putDouble(propKey, node.getValue())
}
} else if (node is ColorAnimatedNode) {
result.putInt(propKey, node.color)
}
} else {
result.putMap(propKey, collectViewUpdatesHelper(map))
}
}
ReadableType.Array ->
result.putArray(propKey, collectViewUpdatesHelper(source.getArray(propKey)))
}
}
return result
}
override fun prettyPrint(): String = "ObjectAnimatedNode[$tag]: mConfig: $configClone"
companion object {
private const val VALUE_KEY = "value"
private const val NODE_TAG_KEY = "nodeTag"
}
}

View File

@@ -0,0 +1,123 @@
/*
* 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.animated
import android.view.View
import com.facebook.react.bridge.JSApplicationIllegalArgumentException
import com.facebook.react.bridge.JavaOnlyMap
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.bridge.UIManager
import com.facebook.react.uimanager.common.UIManagerType
import com.facebook.react.uimanager.common.ViewUtil.getUIManagerType
/**
* Animated node that represents view properties. There is a special handling logic implemented for
* the nodes of this type in [NativeAnimatedNodesManager] that is responsible for extracting a map
* of updated properties, which can be then passed down to the view.
*/
internal class PropsAnimatedNode(
config: ReadableMap,
private val nativeAnimatedNodesManager: NativeAnimatedNodesManager,
) : AnimatedNode() {
private var connectedViewTag = -1
private val propNodeMapping: MutableMap<String, Int>
private val propMap = JavaOnlyMap()
private var connectedViewUIManager: UIManager? = null
val connectedView: View?
// resolveView throws an [IllegalViewOperationException] when the view doesn't exist
// (this can happen if the surface is being deallocated).
get() = runCatching { connectedViewUIManager?.resolveView(connectedViewTag) }.getOrNull()
init {
val props = config.getMap("props")
val iter = props?.keySetIterator()
propNodeMapping = mutableMapOf()
while (iter != null && iter.hasNextKey()) {
val propKey = iter.nextKey()
val nodeIndex = props.getInt(propKey)
propNodeMapping[propKey] = nodeIndex
}
}
fun connectToView(viewTag: Int, uiManager: UIManager?) {
if (connectedViewTag != -1) {
throw JSApplicationIllegalArgumentException(
"Animated node $tag is already attached to a view: $connectedViewTag"
)
}
connectedViewTag = viewTag
connectedViewUIManager = uiManager
}
fun disconnectFromView(viewTag: Int) {
if (connectedViewTag != viewTag && connectedViewTag != -1) {
throw JSApplicationIllegalArgumentException(
"Attempting to disconnect view that has " +
"not been connected with the given animated node: $viewTag " +
"but is connected to view $connectedViewTag"
)
}
connectedViewTag = -1
}
fun restoreDefaultValues() {
// Cannot restore default values if this view has already been disconnected.
if (connectedViewTag == -1) {
return
}
// Don't restore default values in Fabric.
// In Non-Fabric this had the effect of "restore the value to whatever the value was on the
// ShadowNode instead of in the View hierarchy". However, "synchronouslyUpdateViewOnUIThread"
// will not have that impact on Fabric, because the FabricUIManager doesn't have access to the
// ShadowNode layer.
if (getUIManagerType(connectedViewTag) == UIManagerType.FABRIC) {
return
}
val it = propMap.keySetIterator()
while (it.hasNextKey()) {
propMap.putNull(it.nextKey())
}
connectedViewUIManager?.synchronouslyUpdateViewOnUIThread(connectedViewTag, propMap)
}
fun updateView() {
if (connectedViewTag == -1) {
return
}
for ((key, value) in propNodeMapping) {
val node = nativeAnimatedNodesManager.getNodeById(value)
requireNotNull(node) { "Mapped property node does not exist" }
if (node is StyleAnimatedNode) {
node.collectViewUpdates(propMap)
} else if (node is ValueAnimatedNode) {
val animatedObject = node.getAnimatedObject()
if (animatedObject is Int) {
propMap.putInt(key, animatedObject)
} else if (animatedObject is String) {
propMap.putString(key, animatedObject)
} else {
propMap.putDouble(key, node.getValue())
}
} else if (node is ColorAnimatedNode) {
propMap.putInt(key, node.color)
} else if (node is ObjectAnimatedNode) {
node.collectViewUpdates(key, propMap)
} else {
throw IllegalArgumentException(
"Unsupported type of node used in property node ${node.javaClass}"
)
}
}
connectedViewUIManager?.synchronouslyUpdateViewOnUIThread(connectedViewTag, propMap)
}
override fun prettyPrint(): String =
"PropsAnimatedNode[$tag] connectedViewTag: $connectedViewTag " +
"propNodeMapping: $propNodeMapping propMap: $propMap"
}

View File

@@ -0,0 +1,187 @@
/*
* 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.animated
import com.facebook.react.bridge.ReadableMap
import kotlin.math.*
/**
* Implementation of [AnimationDriver] providing support for spring animations. The implementation
* has been copied from android implementation of Rebound library (see
* [http://facebook.github.io/rebound/](http://facebook.github.io/rebound/))
*/
internal class SpringAnimation(config: ReadableMap) : AnimationDriver() {
// storage for the current and prior physics state while integration is occurring
private data class PhysicsState(var position: Double = 0.0, var velocity: Double = 0.0)
private var lastTime: Long = 0
private var springStarted = false
// configuration
private var springStiffness = 0.0
private var springDamping = 0.0
private var springMass = 0.0
private var initialVelocity = 0.0
private var overshootClampingEnabled = false
// all physics simulation objects are final and reused in each processing pass
private val currentState = PhysicsState()
private var startValue = 0.0
private var endValue = 0.0
// thresholds for determining when the spring is at rest
private var restSpeedThreshold = 0.0
private var displacementFromRestThreshold = 0.0
private var timeAccumulator = 0.0
// for controlling loop
private var iterations = 0
private var currentLoop = 0
private var originalValue = 0.0
private val isAtRest: Boolean
/**
* check if the current state is at rest
*
* @return is the spring at rest
*/
get() =
(abs(currentState.velocity) <= restSpeedThreshold &&
(getDisplacementDistanceForState(currentState) <= displacementFromRestThreshold ||
springStiffness == 0.0))
/* Check if the spring is overshooting beyond its target. */
private val isOvershooting: Boolean
get() =
(springStiffness > 0 &&
((startValue < endValue && currentState.position > endValue) ||
(startValue > endValue && currentState.position < endValue)))
init {
currentState.velocity = config.getDouble("initialVelocity")
resetConfig(config)
}
override fun resetConfig(config: ReadableMap) {
springStiffness = config.getDouble("stiffness")
springDamping = config.getDouble("damping")
springMass = config.getDouble("mass")
initialVelocity = currentState.velocity
endValue = config.getDouble("toValue")
restSpeedThreshold = config.getDouble("restSpeedThreshold")
displacementFromRestThreshold = config.getDouble("restDisplacementThreshold")
overshootClampingEnabled = config.getBoolean("overshootClamping")
iterations = if (config.hasKey("iterations")) config.getInt("iterations") else 1
hasFinished = iterations == 0
currentLoop = 0
timeAccumulator = 0.0
springStarted = false
}
override fun runAnimationStep(frameTimeNanos: Long) {
val animatedValue = requireNotNull(animatedValue) { "Animated value should not be null" }
val frameTimeMillis = frameTimeNanos / 1_000_000
if (!springStarted) {
if (currentLoop == 0) {
originalValue = animatedValue.nodeValue
currentLoop = 1
}
currentState.position = animatedValue.nodeValue
startValue = currentState.position
lastTime = frameTimeMillis
timeAccumulator = 0.0
springStarted = true
}
advance((frameTimeMillis - lastTime) / 1000.0)
lastTime = frameTimeMillis
animatedValue.nodeValue = currentState.position
if (isAtRest) {
if (iterations == -1 || currentLoop < iterations) { // looping animation, return to start
springStarted = false
animatedValue.nodeValue = originalValue
currentLoop++
} else { // animation has completed
hasFinished = true
}
}
}
/**
* get the displacement from rest for a given physics state
*
* @param state the state to measure from
* @return the distance displaced by
*/
private fun getDisplacementDistanceForState(state: PhysicsState): Double =
abs(endValue - state.position)
private fun advance(realDeltaTime: Double) {
if (isAtRest) {
return
}
// clamp the amount of realTime to simulate to avoid stuttering in the UI. We should be able
// to catch up in a subsequent advance if necessary.
var adjustedDeltaTime = realDeltaTime
if (realDeltaTime > MAX_DELTA_TIME_SEC) {
adjustedDeltaTime = MAX_DELTA_TIME_SEC
}
timeAccumulator += adjustedDeltaTime
val c = springDamping
val m = springMass
val k = springStiffness
val v0 = -initialVelocity
val zeta = c / (2 * sqrt(k * m))
val omega0 = sqrt(k / m)
val omega1 = omega0 * sqrt(1.0 - zeta * zeta)
val x0 = endValue - startValue
val velocity: Double
val position: Double
val t = timeAccumulator
if (zeta < 1) {
// Under damped
val envelope = exp(-zeta * omega0 * t)
position =
(endValue -
envelope *
((v0 + zeta * omega0 * x0) / omega1 * sin(omega1 * t) + x0 * cos(omega1 * t)))
// This looks crazy -- it's actually just the derivative of the
// oscillation function
velocity =
((zeta *
omega0 *
envelope *
(sin(omega1 * t) * (v0 + zeta * omega0 * x0) / omega1 + x0 * cos(omega1 * t))) -
envelope *
(cos(omega1 * t) * (v0 + zeta * omega0 * x0) - omega1 * x0 * sin(omega1 * t)))
} else {
// Critically damped spring
val envelope = exp(-omega0 * t)
position = endValue - envelope * (x0 + (v0 + omega0 * x0) * t)
velocity = envelope * (v0 * (t * omega0 - 1) + t * x0 * (omega0 * omega0))
}
currentState.position = position
currentState.velocity = velocity
// End the spring immediately if it is overshooting and overshoot clamping is enabled.
// Also make sure that if the spring was considered within a resting threshold that it's now
// snapped to its end value.
if (isAtRest || (overshootClampingEnabled && isOvershooting)) {
// Don't call setCurrentValue because that forces a call to onSpringUpdate
if (springStiffness > 0) {
startValue = endValue
currentState.position = endValue
} else {
endValue = currentState.position
startValue = endValue
}
currentState.velocity = 0.0
}
}
companion object {
// maximum amount of time to simulate per physics iteration in seconds (4 frames at 60 FPS)
private const val MAX_DELTA_TIME_SEC = 0.064
}
}

View File

@@ -0,0 +1,62 @@
/*
* 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.animated
import com.facebook.react.bridge.JavaOnlyMap
import com.facebook.react.bridge.ReadableMap
/**
* Native counterpart of style animated node (see AnimatedStyle class in AnimatedImplementation.js)
*/
internal class StyleAnimatedNode(
config: ReadableMap,
private val nativeAnimatedNodesManager: NativeAnimatedNodesManager,
) : AnimatedNode() {
private val propMapping: Map<String, Int>
init {
val style = config.getMap("style")
val iter = style?.keySetIterator()
propMapping =
buildMap() {
while (iter != null && iter.hasNextKey()) {
val propKey = iter.nextKey()
put(propKey, style.getInt(propKey))
}
}
}
fun collectViewUpdates(propsMap: JavaOnlyMap) {
for ((key, value) in propMapping) {
val node = nativeAnimatedNodesManager.getNodeById(value)
requireNotNull(node) { "Mapped style node does not exist" }
if (node is TransformAnimatedNode) {
node.collectViewUpdates(propsMap)
} else if (node is ValueAnimatedNode) {
val animatedObject = node.getAnimatedObject()
if (animatedObject is Int) {
propsMap.putInt(key, animatedObject)
} else if (animatedObject is String) {
propsMap.putString(key, animatedObject)
} else {
propsMap.putDouble(key, node.getValue())
}
} else if (node is ColorAnimatedNode) {
propsMap.putInt(key, node.color)
} else if (node is ObjectAnimatedNode) {
node.collectViewUpdates(key, propsMap)
} else {
throw IllegalArgumentException(
"Unsupported type of node used in property node ${node.javaClass}"
)
}
}
}
override fun prettyPrint(): String = "StyleAnimatedNode[$tag] mPropMapping: $propMapping"
}

View File

@@ -0,0 +1,53 @@
/*
* 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.animated
import com.facebook.react.bridge.JSApplicationCausedNativeException
import com.facebook.react.bridge.ReadableMap
/**
* Animated node that plays a role of value aggregator. It takes two or more value nodes as an input
* and outputs a difference of values outputted by those nodes.
*/
internal class SubtractionAnimatedNode(
config: ReadableMap,
private val nativeAnimatedNodesManager: NativeAnimatedNodesManager,
) : ValueAnimatedNode() {
private val inputNodes: IntArray
init {
val input = config.getArray("input")
inputNodes =
if (input == null) {
IntArray(0)
} else {
IntArray(input.size()) { i -> input.getInt(i) }
}
}
override fun update() {
for (i in inputNodes.indices) {
val animatedNode = nativeAnimatedNodesManager.getNodeById(inputNodes[i])
if (animatedNode != null && animatedNode is ValueAnimatedNode) {
val value = animatedNode.getValue()
if (i == 0) {
nodeValue = value
} else {
nodeValue -= value
}
} else {
throw JSApplicationCausedNativeException(
"Illegal node ID set as an input for Animated.subtract node"
)
}
}
}
override fun prettyPrint(): String =
"SubtractionAnimatedNode[$tag]: input nodes: $inputNodes - super: ${super.prettyPrint()}"
}

View File

@@ -0,0 +1,40 @@
/*
* 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.animated
import com.facebook.react.bridge.JavaOnlyMap
import com.facebook.react.bridge.ReadableMap
internal class TrackingAnimatedNode(
config: ReadableMap,
private val nativeAnimatedNodesManager: NativeAnimatedNodesManager,
) : AnimatedNode() {
private val animationConfig: JavaOnlyMap = JavaOnlyMap.deepClone(config.getMap("animationConfig"))
private val animationId: Int = config.getInt("animationId")
private val toValueNode: Int = config.getInt("toValue")
private val valueNode: Int = config.getInt("value")
override fun update() {
val toValue = nativeAnimatedNodesManager.getNodeById(toValueNode)
val valAnimatedNode = toValue as? ValueAnimatedNode
if (valAnimatedNode != null) {
animationConfig.putDouble("toValue", valAnimatedNode.getValue())
} else {
val drivenNode = nativeAnimatedNodesManager.getNodeById(valueNode) as? ValueAnimatedNode
if (drivenNode == null) {
return
}
animationConfig.putDouble("toValue", drivenNode.getValue())
}
nativeAnimatedNodesManager.startAnimatingNode(animationId, valueNode, animationConfig, null)
}
override fun prettyPrint(): String =
"TrackingAnimatedNode[$tag]: animationID: $animationId toValueNode: $toValueNode " +
"valueNode: $valueNode animationConfig: $animationConfig"
}

View File

@@ -0,0 +1,88 @@
/*
* 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.animated
import com.facebook.react.bridge.JavaOnlyArray
import com.facebook.react.bridge.JavaOnlyMap
import com.facebook.react.bridge.ReadableMap
/**
* Native counterpart of transform animated node (see AnimatedTransform class in
* AnimatedImplementation.js)
*/
internal class TransformAnimatedNode(
config: ReadableMap,
private val nativeAnimatedNodesManager: NativeAnimatedNodesManager,
) : AnimatedNode() {
private val transformConfigs: List<TransformConfig>
init {
val transforms = config.getArray("transforms")
transformConfigs =
if (transforms == null) emptyList()
else
List(transforms.size()) { i ->
val transformConfigMap = checkNotNull(transforms.getMap(i))
val property = transformConfigMap.getString("property")
val type = transformConfigMap.getString("type")
if (type == "animated") {
val transformConfig = AnimatedTransformConfig()
transformConfig.property = property
transformConfig.nodeTag = transformConfigMap.getInt("nodeTag")
transformConfig
} else {
val transformConfig = StaticTransformConfig()
transformConfig.property = property
transformConfig.value = transformConfigMap.getDouble("value")
transformConfig
}
}
}
fun collectViewUpdates(propsMap: JavaOnlyMap) {
val transforms =
List(transformConfigs.size) { i ->
val transformConfig = transformConfigs[i]
val transform =
if (transformConfig is AnimatedTransformConfig) {
val nodeTag = transformConfig.nodeTag
val node = nativeAnimatedNodesManager.getNodeById(nodeTag)
if (node == null) {
throw IllegalArgumentException("Mapped style node does not exist")
} else if (node is ValueAnimatedNode) {
node.getValue()
} else {
throw IllegalArgumentException(
"Unsupported type of node used as a transform child " +
"node " +
node.javaClass
)
}
} else {
(transformConfig as StaticTransformConfig).value
}
JavaOnlyMap.of(transformConfig.property, transform)
}
propsMap.putArray("transform", JavaOnlyArray.from(transforms))
}
override fun prettyPrint(): String =
"TransformAnimatedNode[$tag]: transformConfigs: $transformConfigs"
private open inner class TransformConfig {
var property: String? = null
}
private inner class AnimatedTransformConfig : TransformConfig() {
var nodeTag = 0
}
private inner class StaticTransformConfig : TransformConfig() {
var value = 0.0
}
}

View File

@@ -0,0 +1,49 @@
/*
* 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.animated
import com.facebook.react.bridge.ReadableMap
/**
* Basic type of animated node that maps directly from {@code Animated.Value(x)} of Animated.js
* library.
*/
internal open class ValueAnimatedNode(config: ReadableMap? = null) : AnimatedNode() {
@JvmField internal var nodeValue: Double = config?.getDouble("value") ?: Double.NaN
@JvmField internal var offset: Double = config?.getDouble("offset") ?: 0.0
private var valueListener: AnimatedNodeValueListener? = null
fun getValue(): Double {
if ((offset + nodeValue).isNaN()) {
this.update()
}
return offset + nodeValue
}
open fun getAnimatedObject(): Any? = null
fun flattenOffset() {
nodeValue += offset
offset = 0.0
}
fun extractOffset() {
offset += nodeValue
nodeValue = 0.0
}
fun onValueUpdate() {
valueListener?.onValueUpdate(getValue() - offset, offset)
}
fun setValueListener(listener: AnimatedNodeValueListener?) {
valueListener = listener
}
override fun prettyPrint(): String = "ValueAnimatedNode[$tag]: value: $nodeValue offset: $offset"
}

View File

@@ -0,0 +1,26 @@
/*
* 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.bridge
import android.app.Activity
import android.content.Intent
/**
* Listener for receiving activity events. Consider using [BaseActivityEventListener] if you're not
* interested in all the events sent to this interface.
*/
public interface ActivityEventListener {
/** Called when host (activity/service) receives an [Activity.onActivityResult] call. */
public fun onActivityResult(activity: Activity, requestCode: Int, resultCode: Int, data: Intent?)
/** Called when a new intent is passed to the activity. */
public fun onNewIntent(intent: Intent)
/** Called when host activity receives an [Activity.onUserLeaveHint] call. */
public fun onUserLeaveHint(activity: Activity): Unit = Unit
}

View File

@@ -0,0 +1,358 @@
/*
* 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.bridge
import android.os.Bundle
import android.os.Parcelable
import com.facebook.proguard.annotations.DoNotStrip
import java.util.AbstractList
import kotlin.math.round
@DoNotStrip
public object Arguments {
@Suppress("UNCHECKED_CAST")
private fun makeNativeObject(value: Any?): Any? =
when {
value == null -> null
value is Float || value is Long || value is Byte || value is Short ->
(value as Number).toDouble()
value.javaClass.isArray -> makeNativeArray<Any>(value)
value is List<*> -> makeNativeArray(value)
value is Map<*, *> -> makeNativeMap(value as Map<String, Any?>)
value is Bundle -> makeNativeMap(value)
value is JavaOnlyMap -> makeNativeMap(value.toHashMap())
value is JavaOnlyArray -> makeNativeArray(value.toArrayList())
else -> value // Boolean, Integer, Double, String, WritableNativeArray, WritableNativeMap
}
/**
* This method converts a List into a NativeArray. The data types supported are boolean, int,
* float, double, and String. List, Map, and Bundle objects, as well as arrays, containing values
* of the above types and/or null, or any recursive arrangement of these, are also supported. The
* best way to think of this is a way to generate a Java representation of a json list, from Java
* types which have a natural representation in json.
*/
@JvmStatic
public fun makeNativeArray(objects: List<*>?): WritableNativeArray {
val nativeArray = WritableNativeArray()
if (objects == null) {
return nativeArray
}
for (elem in objects) {
when (val value = makeNativeObject(elem)) {
null -> nativeArray.pushNull()
is Boolean -> nativeArray.pushBoolean(value)
is Int -> nativeArray.pushInt(value)
is Double -> nativeArray.pushDouble(value)
is String -> nativeArray.pushString(value)
is WritableNativeArray -> nativeArray.pushArray(value)
is WritableNativeMap -> nativeArray.pushMap(value)
else -> throw IllegalArgumentException("Could not convert ${value.javaClass}")
}
}
return nativeArray
}
/**
* This overload is like the above, but uses reflection to operate on any primitive or object
* type.
*/
@JvmStatic
public fun <T> makeNativeArray(objects: Any?): WritableNativeArray {
if (objects == null) {
return WritableNativeArray()
}
// No explicit check for objects's type here. If it's not an array, the
// Array methods will throw IllegalArgumentException.
return makeNativeArray(
object : AbstractList<Any?>() {
override val size: Int
get() = java.lang.reflect.Array.getLength(objects)
override fun get(index: Int): Any? = java.lang.reflect.Array.get(objects, index)
}
)
}
private fun addEntry(nativeMap: WritableNativeMap, key: String, value: Any?) {
when (val nativeObjectValue = makeNativeObject(value)) {
null -> nativeMap.putNull(key)
is Boolean -> nativeMap.putBoolean(key, nativeObjectValue)
is Int -> nativeMap.putInt(key, nativeObjectValue)
is Number -> nativeMap.putDouble(key, nativeObjectValue.toDouble())
is String -> nativeMap.putString(key, nativeObjectValue)
is WritableNativeArray -> nativeMap.putArray(key, nativeObjectValue)
is WritableNativeMap -> nativeMap.putMap(key, nativeObjectValue)
else -> throw IllegalArgumentException("Could not convert ${nativeObjectValue.javaClass}")
}
}
/**
* This method converts a Map into a NativeMap. Value types are supported as with makeNativeArray.
* The best way to think of this is a way to generate a Java representation of a json object, from
* Java types which have a natural representation in json.
*/
@DoNotStrip
@JvmStatic
public fun makeNativeMap(objects: Map<String, Any?>?): WritableNativeMap {
val nativeMap = WritableNativeMap()
if (objects == null) {
return nativeMap
}
for ((key, value) in objects) {
addEntry(nativeMap, key, value)
}
return nativeMap
}
/** Like the above, but takes a Bundle instead of a Map. */
@DoNotStrip
@JvmStatic
@Suppress("DEPRECATION")
public fun makeNativeMap(bundle: Bundle?): WritableNativeMap {
val nativeMap = WritableNativeMap()
if (bundle == null) {
return nativeMap
}
for (key in bundle.keySet()) {
addEntry(nativeMap, key, bundle[key])
}
return nativeMap
}
/** This method should be used when you need to stub out creating NativeArrays in unit tests. */
@JvmStatic public fun createArray(): WritableArray = WritableNativeArray()
/** This method should be used when you need to stub out creating NativeMaps in unit tests. */
@JvmStatic public fun createMap(): WritableMap = WritableNativeMap()
@Suppress("UNCHECKED_CAST")
@JvmStatic
@Deprecated(
"Use fromJavaArgs(Array<Any?>) instead. This method is added only to retain compatibility with Java consumers."
)
public fun fromJavaArgs(args: Any?): WritableNativeArray = fromJavaArgs(args as Array<Any?>)
@JvmStatic
public fun fromJavaArgs(args: Array<Any?>): WritableNativeArray {
val arguments = WritableNativeArray()
for (i in args.indices) {
val argument = args[i]
when (val argumentClass = argument?.javaClass) {
null -> arguments.pushNull()
Boolean::class.java,
java.lang.Boolean::class.java -> arguments.pushBoolean(argument as Boolean)
Int::class.java,
java.lang.Integer::class.java -> arguments.pushDouble((argument as Number).toDouble())
Double::class.java,
java.lang.Double::class.java -> arguments.pushDouble((argument as Double))
Float::class.java -> arguments.pushDouble((argument as Float).toDouble())
java.lang.Float::class.java -> arguments.pushDouble((argument as Float).toDouble())
String::class.java -> arguments.pushString(argument.toString())
WritableNativeMap::class.java -> arguments.pushMap(argument as WritableNativeMap)
WritableNativeArray::class.java -> arguments.pushArray(argument as WritableNativeArray)
else -> throw RuntimeException("Cannot convert argument of type $argumentClass")
}
}
return arguments
}
/**
* Convert an array to a [WritableArray].
*
* @param array the array to convert. Supported types are: `String[]`, `Bundle[]`, `int[]`,
* `float[]`, `double[]`, `boolean[]`.
* @return the converted [WritableArray]
* @throws IllegalArgumentException if the passed object is none of the above types
*/
@JvmStatic
@Suppress("UNCHECKED_CAST")
public fun fromArray(array: Any): WritableArray {
val catalystArray = createArray()
when {
array is Array<*> && array.isArrayOf<String>() -> {
for (v in array as Array<String?>) {
catalystArray.pushString(v)
}
}
array is Array<*> && array.isArrayOf<Bundle>() -> {
for (v in array as Array<Bundle>) {
catalystArray.pushMap(fromBundle(v))
}
}
array is IntArray -> {
for (v in array) {
catalystArray.pushInt(v)
}
}
array is FloatArray -> {
for (v in array) {
catalystArray.pushDouble(v.toDouble())
}
}
array is DoubleArray -> {
for (v in array) {
catalystArray.pushDouble(v)
}
}
array is BooleanArray -> {
for (v in array) {
catalystArray.pushBoolean(v)
}
}
array is Array<*> && array.isArrayOf<Parcelable>() -> {
for (v in array as Array<Parcelable>) {
if (v is Bundle) {
catalystArray.pushMap(fromBundle(v))
} else {
throw IllegalArgumentException("Unexpected array member type ${v.javaClass}")
}
}
}
else -> throw IllegalArgumentException("Unknown array type ${array.javaClass}")
}
return catalystArray
}
/**
* Convert a [List] to a [WritableArray].
*
* @param list the list to convert. Supported value types are: `null`, `String`, `Bundle`, `List`,
* `Number`, `Boolean`, and all array types supported in [.fromArray].
* @return the converted [WritableArray]
* @throws IllegalArgumentException if one of the values from the passed list is none of the above
* types
*/
@JvmStatic
public fun fromList(list: List<*>): WritableArray {
val catalystArray = createArray()
for (obj in list) {
when {
obj == null -> catalystArray.pushNull()
obj.javaClass.isArray -> catalystArray.pushArray(fromArray(obj))
obj is Bundle -> catalystArray.pushMap(fromBundle(obj))
obj is List<*> -> catalystArray.pushArray(fromList(obj))
obj is String -> catalystArray.pushString(obj)
obj is Int -> catalystArray.pushInt(obj)
obj is Number -> catalystArray.pushDouble(obj.toDouble())
obj is Boolean -> catalystArray.pushBoolean(obj)
else -> throw IllegalArgumentException("Unknown value type ${obj.javaClass}")
}
}
return catalystArray
}
/**
* Convert a [Bundle] to a [WritableMap]. Supported key types in the bundle are:
* * primitive types: int, float, double, boolean
* * arrays supported by [.fromArray]
* * lists supported by [.fromList]
* * [Bundle] objects that are recursively converted to maps
*
* @param bundle the [Bundle] to convert
* @return the converted [WritableMap]
* @throws IllegalArgumentException if there are keys of unsupported types
*/
@JvmStatic
@Suppress("DEPRECATION")
public fun fromBundle(bundle: Bundle): WritableMap {
val map = createMap()
for (key in bundle.keySet()) {
val value = bundle[key]
when {
value == null -> map.putNull(key)
value.javaClass.isArray -> map.putArray(key, fromArray(value))
value is String -> map.putString(key, value)
value is Number -> {
if (value is Int) {
map.putInt(key, value)
} else {
map.putDouble(key, value.toDouble())
}
}
value is Boolean -> map.putBoolean(key, value)
value is Bundle -> map.putMap(key, fromBundle(value))
value is List<*> -> map.putArray(key, fromList(value))
else -> throw IllegalArgumentException("Could not convert ${value.javaClass}")
}
}
return map
}
/**
* Convert a [WritableArray] to a [ArrayList].
*
* @param readableArray the [WritableArray] to convert.
* @return the converted [ArrayList].
*/
@JvmStatic
@Suppress("REDUNDANT_ELSE_IN_WHEN")
public fun toList(readableArray: ReadableArray?): ArrayList<Any?>? {
if (readableArray == null) {
return null
}
val list: ArrayList<Any?> = ArrayList()
for (i in 0..<readableArray.size()) {
when (readableArray.getType(i)) {
ReadableType.Null -> list.add(null)
ReadableType.Boolean -> list.add(readableArray.getBoolean(i))
ReadableType.Number -> {
val number = readableArray.getDouble(i)
if (number == round(number)) {
// Add as an integer
list.add(number.toInt())
} else {
// Add as a double
list.add(number)
}
}
ReadableType.String -> list.add(readableArray.getString(i))
ReadableType.Map -> list.add(toBundle(readableArray.getMap(i)))
ReadableType.Array -> list.add(toList(readableArray.getArray(i)))
else -> throw IllegalArgumentException("Could not convert object in array.")
}
}
return list
}
/**
* Convert a [WritableMap] to a [Bundle]. Note: Each array is converted to an [ArrayList].
*
* @param readableMap the [WritableMap] to convert.
* @return the converted [Bundle].
*/
@JvmStatic
@Suppress("REDUNDANT_ELSE_IN_WHEN")
public fun toBundle(readableMap: ReadableMap?): Bundle? {
if (readableMap == null) {
return null
}
val iterator = readableMap.keySetIterator()
val bundle = Bundle()
while (iterator.hasNextKey()) {
val key = iterator.nextKey()
when (readableMap.getType(key)) {
ReadableType.Null -> bundle.putString(key, null)
ReadableType.Boolean -> bundle.putBoolean(key, readableMap.getBoolean(key))
ReadableType.Number ->
bundle.putDouble(key, readableMap.getDouble(key)) // Can be int or double.
ReadableType.String -> bundle.putString(key, readableMap.getString(key))
ReadableType.Map -> bundle.putBundle(key, toBundle(readableMap.getMap(key)))
ReadableType.Array -> bundle.putSerializable(key, toList(readableMap.getArray(key)))
else -> throw IllegalArgumentException("Could not convert object with key: $key.")
}
}
return bundle
}
}

View File

@@ -0,0 +1,14 @@
/*
* 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.bridge
/**
* Like [AssertionError] but extends RuntimeException so that it may be caught by a
* [JSExceptionHandler]. See that class for more details. Used in conjunction with [SoftAssertions].
*/
public class AssertionException(message: String) : RuntimeException(message)

View File

@@ -0,0 +1,30 @@
/*
* 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.bridge
import android.app.Activity
import android.content.Intent
/** An empty implementation of [ActivityEventListener]. */
public open class BaseActivityEventListener : ActivityEventListener {
@Suppress("UNUSED_PARAMETER")
@Deprecated(
"Use onActivityResult(Activity, Int, Int, Intent) instead.",
ReplaceWith("onActivityResult(activity, requestCode, resultCode, data)"),
)
public open fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent): Unit = Unit
public override fun onActivityResult(
activity: Activity,
requestCode: Int,
resultCode: Int,
data: Intent?,
): Unit = Unit
public override fun onNewIntent(intent: Intent): Unit = Unit
}

View File

@@ -0,0 +1,140 @@
/*
* 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.bridge;
import static com.facebook.infer.annotation.ThreadConfined.ANY;
import androidx.annotation.Nullable;
import com.facebook.common.logging.FLog;
import com.facebook.infer.annotation.Assertions;
import com.facebook.infer.annotation.Nullsafe;
import com.facebook.infer.annotation.ThreadConfined;
import com.facebook.proguard.annotations.DoNotStrip;
import com.facebook.react.common.ReactConstants;
import com.facebook.react.common.annotations.StableReactNativeAPI;
import com.facebook.react.common.build.ReactBuildConfig;
import java.util.Map;
/**
* Base class for Catalyst native modules whose implementations are written in Java. Default
* implementations for {@link #initialize} and {@link #onCatalystInstanceDestroy} are provided for
* convenience. Subclasses which override these don't need to call {@code super} in case of
* overriding those methods as implementation of those methods is empty.
*
* <p>BaseJavaModules can be linked to Fragments' lifecycle events, {@link CatalystInstance}
* creation and destruction, by being called on the appropriate method when a life cycle event
* occurs.
*
* <p>Native methods can be exposed to JS with {@link ReactMethod} annotation. Those methods may
* only use limited number of types for their arguments:
*
* <ol>
* <li>primitives (boolean, int, float, double
* <li>{@link String} mapped from JS string
* <li>{@link ReadableArray} mapped from JS Array
* <li>{@link ReadableMap} mapped from JS Object
* <li>{@link Callback} mapped from js function and can be used only as a last parameter or in the
* case when it express success & error callback pair as two last arguments respectively.
* </ol>
*
* <p>All methods exposed as native to JS with {@link ReactMethod} annotation must return {@code
* void}.
*
* <p>Please note that it is not allowed to have multiple methods annotated with {@link ReactMethod}
* with the same name.
*/
@Nullsafe(Nullsafe.Mode.LOCAL)
@StableReactNativeAPI
public abstract class BaseJavaModule implements NativeModule {
// taken from Libraries/Utilities/MessageQueue.js
public static final String METHOD_TYPE_ASYNC = "async";
public static final String METHOD_TYPE_PROMISE = "promise";
public static final String METHOD_TYPE_SYNC = "sync";
protected @Nullable CxxCallbackImpl mEventEmitterCallback;
private final @Nullable ReactApplicationContext mReactApplicationContext;
public BaseJavaModule() {
this(null);
}
public BaseJavaModule(@Nullable ReactApplicationContext reactContext) {
mReactApplicationContext = reactContext;
}
/**
* @return a map of constants this module exports to JS. Supports JSON types.
*/
public @Nullable Map<String, Object> getConstants() {
return null;
}
@Override
public void initialize() {
// do nothing
}
@Override
public boolean canOverrideExistingModule() {
return false;
}
/**
* The CatalystInstance is going away with Venice. Therefore, the TurboModule infra introduces the
* invalidate() method to allow NativeModules to clean up after themselves.
*/
@Override
public void invalidate() {}
/**
* Subclasses can use this method to access {@link ReactApplicationContext} passed as a
* constructor.
*/
protected final ReactApplicationContext getReactApplicationContext() {
return Assertions.assertNotNull(
mReactApplicationContext,
"Tried to get ReactApplicationContext even though NativeModule wasn't instantiated with"
+ " one");
}
/**
* Subclasses can use this method to access {@link ReactApplicationContext} passed as a
* constructor. Use this version to check that the underlying React Instance is active before
* returning, and automatically have SoftExceptions or debug information logged for you. Consider
* using this whenever calling ReactApplicationContext methods that require the React instance be
* alive.
*
* <p>This can return null at any time, but especially during teardown methods it's
* possible/likely.
*
* <p>Threading implications have not been analyzed fully yet, so assume this method is not
* thread-safe.
*/
@ThreadConfined(ANY)
protected @Nullable final ReactApplicationContext getReactApplicationContextIfActiveOrWarn() {
if (mReactApplicationContext != null && mReactApplicationContext.hasActiveReactInstance()) {
return mReactApplicationContext;
}
// We want to collect data about how often this happens, but SoftExceptions will cause a crash
// in debug mode, which isn't usually desirable.
String msg = "React Native Instance has already disappeared: requested by " + getName();
if (ReactBuildConfig.DEBUG) {
FLog.w(ReactConstants.TAG, msg);
} else {
ReactSoftExceptionLogger.logSoftException(ReactConstants.TAG, new RuntimeException(msg));
}
return null;
}
@DoNotStrip
protected void setEventEmitterCallback(CxxCallbackImpl eventEmitterCallback) {
mEventEmitterCallback = eventEmitterCallback;
}
}

View File

@@ -0,0 +1,315 @@
/*
* 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.bridge;
import static com.facebook.infer.annotation.ThreadConfined.UI;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.facebook.common.logging.FLog;
import com.facebook.infer.annotation.Assertions;
import com.facebook.infer.annotation.Nullsafe;
import com.facebook.infer.annotation.ThreadConfined;
import com.facebook.proguard.annotations.DoNotStrip;
import com.facebook.react.bridge.queue.ReactQueueConfiguration;
import com.facebook.react.common.ReactConstants;
import com.facebook.react.common.annotations.FrameworkAPI;
import com.facebook.react.common.annotations.UnstableReactNativeAPI;
import com.facebook.react.common.annotations.VisibleForTesting;
import com.facebook.react.common.annotations.internal.LegacyArchitecture;
import com.facebook.react.common.annotations.internal.LegacyArchitectureLogLevel;
import com.facebook.react.common.annotations.internal.LegacyArchitectureLogger;
import com.facebook.react.turbomodule.core.interfaces.CallInvokerHolder;
import java.util.Collection;
import java.util.Objects;
/**
* This is the bridge-specific concrete subclass of ReactContext. ReactContext has many methods that
* delegate to the react instance. This subclass implements those methods, by delegating to the
* CatalystInstance. If you need to create a ReactContext within an "bridge context", please create
* BridgeReactContext.
*
* @deprecated This class is deprecated in the New Architecture and will be replaced by {@link
* com.facebook.react.runtime.BridgelessReactContext}
*/
@VisibleForTesting
@LegacyArchitecture(logLevel = LegacyArchitectureLogLevel.ERROR)
@Deprecated(
since = "This class is part of Legacy Architecture and will be removed in a future release")
@Nullsafe(Nullsafe.Mode.LOCAL)
public class BridgeReactContext extends ReactApplicationContext {
static {
LegacyArchitectureLogger.assertLegacyArchitecture(
"BridgeReactContext", LegacyArchitectureLogLevel.ERROR);
}
@DoNotStrip
public interface RCTDeviceEventEmitter extends JavaScriptModule {
void emit(@NonNull String eventName, @Nullable Object data);
}
private static final String TAG = "BridgeReactContext";
private static final String EARLY_JS_ACCESS_EXCEPTION_MESSAGE =
"Tried to access a JS module before the React instance was fully set up. Calls to "
+ "ReactContext#getJSModule should only happen once initialize() has been called on your "
+ "native module.";
private static final String LATE_JS_ACCESS_EXCEPTION_MESSAGE =
"Tried to access a JS module after the React instance was destroyed.";
private static final String EARLY_NATIVE_MODULE_EXCEPTION_MESSAGE =
"Trying to call native module before CatalystInstance has been set!";
private static final String LATE_NATIVE_MODULE_EXCEPTION_MESSAGE =
"Trying to call native module after CatalystInstance has been destroyed!";
private volatile boolean mDestroyed = false;
private @Nullable CatalystInstance mCatalystInstance;
public BridgeReactContext(Context context) {
super(context);
}
/** Set and initialize CatalystInstance for this Context. This should be called exactly once. */
public void initializeWithInstance(CatalystInstance catalystInstance) {
if (catalystInstance == null) {
throw new IllegalArgumentException("CatalystInstance cannot be null.");
}
if (mCatalystInstance != null) {
throw new IllegalStateException("ReactContext has been already initialized");
}
if (mDestroyed) {
ReactSoftExceptionLogger.logSoftException(
TAG,
new IllegalStateException("Cannot initialize ReactContext after it has been destroyed."));
}
mCatalystInstance = catalystInstance;
ReactQueueConfiguration queueConfig = catalystInstance.getReactQueueConfiguration();
initializeMessageQueueThreads(queueConfig);
initializeInteropModules();
}
private void raiseCatalystInstanceMissingException() {
throw new IllegalStateException(
mDestroyed ? LATE_NATIVE_MODULE_EXCEPTION_MESSAGE : EARLY_NATIVE_MODULE_EXCEPTION_MESSAGE);
}
/**
* @return handle to the specified JS module for the CatalystInstance associated with this Context
*/
@Override
public <T extends JavaScriptModule> T getJSModule(Class<T> jsInterface) {
if (mCatalystInstance == null) {
if (mDestroyed) {
throw new IllegalStateException(LATE_JS_ACCESS_EXCEPTION_MESSAGE);
}
throw new IllegalStateException(EARLY_JS_ACCESS_EXCEPTION_MESSAGE);
}
if (mInteropModuleRegistry != null) {
T jsModule = mInteropModuleRegistry.getInteropModule(jsInterface);
if (jsModule != null) {
return jsModule;
}
}
return mCatalystInstance.getJSModule(jsInterface);
}
@Override
public <T extends NativeModule> boolean hasNativeModule(Class<T> nativeModuleInterface) {
if (mCatalystInstance == null) {
raiseCatalystInstanceMissingException();
}
Assertions.assertNotNull(mCatalystInstance);
return mCatalystInstance.hasNativeModule(nativeModuleInterface);
}
@Override
public Collection<NativeModule> getNativeModules() {
if (mCatalystInstance == null) {
raiseCatalystInstanceMissingException();
}
Assertions.assertNotNull(mCatalystInstance);
return mCatalystInstance.getNativeModules();
}
/**
* @return the instance of the specified module interface associated with this ReactContext.
*/
@Override
@Nullable
public <T extends NativeModule> T getNativeModule(Class<T> nativeModuleInterface) {
if (mCatalystInstance == null) {
raiseCatalystInstanceMissingException();
}
Assertions.assertNotNull(mCatalystInstance);
return mCatalystInstance.getNativeModule(nativeModuleInterface);
}
@Override
public @Nullable NativeModule getNativeModule(String moduleName) {
if (mCatalystInstance == null) {
raiseCatalystInstanceMissingException();
}
Assertions.assertNotNull(mCatalystInstance);
return mCatalystInstance.getNativeModule(moduleName);
}
@Override
public CatalystInstance getCatalystInstance() {
return Assertions.assertNotNull(mCatalystInstance);
}
/**
* This API has been deprecated due to naming consideration, please use hasActiveReactInstance()
* instead
*
* @return
*/
@Deprecated
@Override
public boolean hasActiveCatalystInstance() {
return hasActiveReactInstance();
}
/**
* @return true if there is an non-null, alive react native instance
*/
@Override
public boolean hasActiveReactInstance() {
return mCatalystInstance != null && !mCatalystInstance.isDestroyed();
}
/**
* This API has been deprecated due to naming consideration, please use hasReactInstance() instead
*
* @return
*/
@Deprecated
@Override
public boolean hasCatalystInstance() {
return mCatalystInstance != null;
}
@Override
public boolean hasReactInstance() {
return mCatalystInstance != null;
}
/** Destroy this instance, making it unusable. */
@Override
@ThreadConfined(UI)
public void destroy() {
UiThreadUtil.assertOnUiThread();
mDestroyed = true;
if (mCatalystInstance != null) {
mCatalystInstance.destroy();
}
}
/**
* Passes the given exception to the current {@link JSExceptionHandler} if one exists, rethrowing
* otherwise.
*/
@Override
public void handleException(Exception e) {
boolean catalystInstanceVariableExists = mCatalystInstance != null;
boolean isCatalystInstanceAlive = mCatalystInstance != null && !mCatalystInstance.isDestroyed();
boolean hasExceptionHandler = getJSExceptionHandler() != null;
if (isCatalystInstanceAlive && hasExceptionHandler) {
getJSExceptionHandler().handleException(e);
} else {
FLog.e(
ReactConstants.TAG,
"Unable to handle Exception - catalystInstanceVariableExists: "
+ catalystInstanceVariableExists
+ " - isCatalystInstanceAlive: "
+ isCatalystInstanceAlive
+ " - hasExceptionHandler: "
+ hasExceptionHandler,
e);
throw new IllegalStateException(e);
}
}
/**
* @deprecated DO NOT USE, this method will be removed in the near future.
*/
@Deprecated
@Override
public boolean isBridgeless() {
return false;
}
/**
* Get the C pointer (as a long) to the JavaScriptCore context associated with this instance. Use
* the following pattern to ensure that the JS context is not cleared while you are using it:
* JavaScriptContextHolder jsContext = reactContext.getJavaScriptContextHolder()
* synchronized(jsContext) { nativeThingNeedingJsContext(jsContext.get()); }
*/
@Override
@FrameworkAPI
@UnstableReactNativeAPI
public @Nullable JavaScriptContextHolder getJavaScriptContextHolder() {
if (mCatalystInstance != null) {
return mCatalystInstance.getJavaScriptContextHolder();
}
return null;
}
/**
* Returns a hybrid object that contains a pointer to a JS CallInvoker, which is used to schedule
* work on the JS Thread.
*/
@Nullable
@Override
public CallInvokerHolder getJSCallInvokerHolder() {
if (mCatalystInstance != null) {
return mCatalystInstance.getJSCallInvokerHolder();
}
return null;
}
/**
* Get the UIManager for Fabric from the CatalystInstance.
*
* @return The UIManager when CatalystInstance is active.
* @deprecated Do not use this method. Instead use {@link
* com.facebook.react.uimanager.UIManagerHelper} method {@code getUIManager} to get the
* UIManager instance from the current ReactContext.
*/
@Override
@Deprecated
public @Nullable UIManager getFabricUIManager() {
//noinspection deprecation
return Objects.requireNonNull(mCatalystInstance).getFabricUIManager();
}
/**
* Get the sourceURL for the JS bundle from the CatalystInstance. This method is needed for
* compatibility with bridgeless mode, which has no CatalystInstance.
*
* @return The JS bundle URL set when the bundle was loaded
*/
@Override
public @Nullable String getSourceURL() {
return mCatalystInstance == null ? null : mCatalystInstance.getSourceURL();
}
/**
* Register a JS segment after loading it from cache or server, make sure mCatalystInstance is
* properly initialised and not null before calling.
*/
@Override
public void registerSegment(int segmentId, String path, Callback callback) {
Assertions.assertNotNull(mCatalystInstance).registerSegment(segmentId, path);
Assertions.assertNotNull(callback).invoke();
}
}

View File

@@ -0,0 +1,20 @@
/*
* 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.bridge
/**
* Interface that represent javascript callback function which can be passed to the native module as
* a method parameter.
*/
public fun interface Callback {
/**
* Schedule javascript function execution represented by this [Callback] instance
*
* @param args arguments passed to javascript callback method via bridge
*/
public operator fun invoke(vararg args: Any?)
}

View File

@@ -0,0 +1,129 @@
/*
* 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.
*/
@file:Suppress("DEPRECATION")
package com.facebook.react.bridge
import com.facebook.proguard.annotations.DoNotStrip
import com.facebook.react.bridge.queue.ReactQueueConfiguration
import com.facebook.react.common.annotations.internal.LegacyArchitecture
import com.facebook.react.internal.turbomodule.core.interfaces.TurboModuleRegistry
import com.facebook.react.turbomodule.core.interfaces.CallInvokerHolder
import com.facebook.react.turbomodule.core.interfaces.NativeMethodCallInvokerHolder
/**
* A higher level API on top of the asynchronous JSC bridge. This provides an environment allowing
* the invocation of JavaScript methods and lets a set of Java APIs be invocable from JavaScript as
* well.
*/
@Deprecated(
message =
"This class is deprecated, please to migrate to new architecture using [com.facebook.react.defaults.DefaultReactHost] instead."
)
@DoNotStrip
@LegacyArchitecture
public interface CatalystInstance : MemoryPressureListener, JSInstance, JSBundleLoaderDelegate {
public fun runJSBundle()
// Returns the status of running the JS bundle; waits for an answer if runJSBundle is running
public fun hasRunJSBundle(): Boolean
/**
* Return the source URL of the JS Bundle that was run, or `null` if no JS bundle has been run
* yet.
*/
public val sourceURL: String?
// This is called from java code, so it won't be stripped anyway, but proguard will rename it,
// which this prevents.
@DoNotStrip public override fun invokeCallback(callbackID: Int, arguments: NativeArrayInterface)
@DoNotStrip public fun callFunction(module: String, method: String, arguments: NativeArray?)
/**
* Destroys this catalyst instance, waiting for any other threads in ReactQueueConfiguration
* (besides the UI thread) to finish running. Must be called from the UI thread so that we can
* fully shut down other threads.
*/
public fun destroy()
public val isDestroyed: Boolean
/** Initialize all the native modules */
public fun initialize()
public val reactQueueConfiguration: ReactQueueConfiguration
public fun <T : JavaScriptModule> getJSModule(jsInterface: Class<T>): T?
public fun <T : NativeModule> hasNativeModule(nativeModuleInterface: Class<T>): Boolean
public fun <T : NativeModule> getNativeModule(nativeModuleInterface: Class<T>): T?
public fun getNativeModule(moduleName: String): NativeModule?
public val nativeModules: Collection<NativeModule>
/**
* This method permits a CatalystInstance to extend the known Native modules. This provided
* registry contains only the new modules to load.
*/
public fun extendNativeModules(modules: NativeModuleRegistry)
/** This method registers the file path of an additional JS segment by its ID. */
public fun registerSegment(segmentId: Int, path: String)
public fun setGlobalVariable(propName: String, jsonValue: String)
/**
* Do not use this anymore. Use [runtimeExecutor] instead. Get the C pointer (as a long) to the
* JavaScriptCore context associated with this instance.
*
* <p>Use the following pattern to ensure that the JS context is not cleared while you are using
* it: JavaScriptContextHolder jsContext = reactContext.getJavaScriptContextHolder()
* synchronized(jsContext) { nativeThingNeedingJsContext(jsContext.get()); }
*/
@get:Deprecated("Use runtimeExecutor instead.")
public val javaScriptContextHolder: JavaScriptContextHolder
public val runtimeExecutor: RuntimeExecutor?
public val runtimeScheduler: RuntimeScheduler?
/**
* Returns a hybrid object that contains a pointer to a JS CallInvoker, which is used to schedule
* work on the JS Thread. Required for TurboModuleManager initialization.
*/
@get:Deprecated("Use ReactContext.getJSCallInvokerHolder instead")
@Suppress("INAPPLICABLE_JVM_NAME")
@get:JvmName("getJSCallInvokerHolder") // This is needed to keep backward compatibility
public val jsCallInvokerHolder: CallInvokerHolder
/**
* Returns a hybrid object that contains a pointer to a NativeMethodCallInvoker, which is used to
* schedule work on the NativeModules thread. Required for TurboModuleManager initialization.
*/
public val nativeMethodCallInvokerHolder: NativeMethodCallInvokerHolder
@Deprecated(
message =
"This method is deprecated, please to migrate to new architecture using [com.facebook.react.defaults.DefaultReactHost] instead."
)
public fun setTurboModuleRegistry(turboModuleRegistry: TurboModuleRegistry)
@Deprecated(
message =
"This method is deprecated, please to migrate to new architecture using [com.facebook.react.defaults.DefaultReactHost] instead."
)
public fun setFabricUIManager(fabricUIManager: UIManager)
@Deprecated(
message =
"This method is deprecated, please to migrate to new architecture using [com.facebook.react.defaults.DefaultReactHost] instead."
)
public fun getFabricUIManager(): UIManager?
}

View File

@@ -0,0 +1,218 @@
/*
* 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.bridge
import android.content.Context
import android.content.res.Resources
import android.graphics.Color
import android.graphics.ColorSpace
import android.os.Build
import android.util.TypedValue
import androidx.annotation.ColorLong
import androidx.core.content.res.ResourcesCompat
import com.facebook.common.logging.FLog
import com.facebook.react.common.ReactConstants
public object ColorPropConverter {
private fun supportWideGamut(): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
private const val JSON_KEY = "resource_paths"
private const val PREFIX_RESOURCE = "@"
private const val PREFIX_ATTR = "?"
private const val PACKAGE_DELIMITER = ":"
private const val PATH_DELIMITER = "/"
private const val ATTR = "attr"
private const val ATTR_SEGMENT = "attr/"
private fun getColorInteger(value: Any?, context: Context): Int? {
if (value == null) {
return null
}
if (value is Double) {
return value.toInt()
}
checkNotNull(context)
if (value is ReadableMap) {
if (value.hasKey("space")) {
val r = (value.getDouble("r").toFloat() * 255).toInt()
val g = (value.getDouble("g").toFloat() * 255).toInt()
val b = (value.getDouble("b").toFloat() * 255).toInt()
val a = (value.getDouble("a").toFloat() * 255).toInt()
return Color.argb(a, r, g, b)
}
val resourcePaths =
value.getArray(JSON_KEY)
?: throw JSApplicationCausedNativeException(
"ColorValue: The `$JSON_KEY` must be an array of color resource path strings."
)
for (i in 0 until resourcePaths.size()) {
val result = resolveResourcePath(context, resourcePaths.getString(i))
if (result != null) {
return result
}
}
throw JSApplicationCausedNativeException(
"ColorValue: None of the paths in the `$JSON_KEY` array resolved to a color resource."
)
}
throw JSApplicationCausedNativeException("ColorValue: the value must be a number or Object.")
}
@JvmStatic
public fun getColorInstance(value: Any?, context: Context): Color? {
if (value == null) {
return null
}
if (supportWideGamut() && value is Double) {
return Color.valueOf(value.toInt())
}
checkNotNull(context)
if (value is ReadableMap) {
if (supportWideGamut() && value.hasKey("space")) {
val rawColorSpace = value.getString("space")
val isDisplayP3 = rawColorSpace == "display-p3"
val space =
ColorSpace.get(if (isDisplayP3) ColorSpace.Named.DISPLAY_P3 else ColorSpace.Named.SRGB)
val r = value.getDouble("r").toFloat()
val g = value.getDouble("g").toFloat()
val b = value.getDouble("b").toFloat()
val a = value.getDouble("a").toFloat()
@ColorLong val color = Color.pack(r, g, b, a, space)
return Color.valueOf(color)
}
val resourcePaths =
value.getArray(JSON_KEY)
?: throw JSApplicationCausedNativeException(
"ColorValue: The `$JSON_KEY` must be an array of color resource path strings."
)
for (i in 0 until resourcePaths.size()) {
val result = resolveResourcePath(context, resourcePaths.getString(i))
if (supportWideGamut() && result != null) {
return Color.valueOf(result)
}
}
throw JSApplicationCausedNativeException(
"ColorValue: None of the paths in the `$JSON_KEY` array resolved to a color resource."
)
}
throw JSApplicationCausedNativeException("ColorValue: the value must be a number or Object.")
}
@JvmStatic
public fun getColor(value: Any?, context: Context): Int? {
try {
if (supportWideGamut()) {
val color = getColorInstance(value, context)
if (color != null) {
return color.toArgb()
}
}
} catch (ex: JSApplicationCausedNativeException) {
FLog.w(ReactConstants.TAG, ex, "Error extracting color from WideGamut")
}
return getColorInteger(value, context)
}
@JvmStatic
public fun getColor(value: Any?, context: Context, defaultInt: Int): Int {
return try {
getColor(value, context) ?: defaultInt
} catch (e: JSApplicationCausedNativeException) {
FLog.w(ReactConstants.TAG, e, "Error converting ColorValue")
defaultInt
}
}
@JvmStatic
public fun resolveResourcePath(context: Context, resourcePath: String?): Int? {
if (resourcePath.isNullOrEmpty()) {
return null
}
val isResource = resourcePath.startsWith(PREFIX_RESOURCE)
val isThemeAttribute = resourcePath.startsWith(PREFIX_ATTR)
val path = resourcePath.substring(1)
return try {
when {
isResource -> resolveResource(context, path)
isThemeAttribute -> resolveThemeAttribute(context, path)
else -> null
}
} catch (e: Resources.NotFoundException) {
null
}
}
private fun resolveResource(context: Context, resourcePath: String): Int {
val pathTokens = resourcePath.split(PACKAGE_DELIMITER)
var packageName = context.packageName
var resource = resourcePath
if (pathTokens.size > 1) {
packageName = pathTokens[0]
resource = pathTokens[1]
}
val resourceTokens = resource.split(PATH_DELIMITER)
val resourceType = resourceTokens[0]
val resourceName = resourceTokens[1]
val resourceId = context.resources.getIdentifier(resourceName, resourceType, packageName)
return ResourcesCompat.getColor(context.resources, resourceId, context.theme)
}
private fun resolveThemeAttribute(context: Context, resourcePath: String): Int {
val path = resourcePath.replace(ATTR_SEGMENT, "")
val pathTokens = path.split(PACKAGE_DELIMITER)
var packageName = context.packageName
var resourceName = path
if (pathTokens.size > 1) {
packageName = pathTokens[0]
resourceName = pathTokens[1]
}
var resourceId = context.resources.getIdentifier(resourceName, ATTR, packageName)
if (resourceId == 0) {
resourceId = context.resources.getIdentifier(resourceName, ATTR, "android")
}
val outValue = TypedValue()
val theme = context.theme
if (theme.resolveAttribute(resourceId, outValue, true)) {
return outValue.data
}
throw Resources.NotFoundException()
}
}

View File

@@ -0,0 +1,22 @@
/*
* 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.bridge
import com.facebook.jni.HybridClassBase
import com.facebook.proguard.annotations.DoNotStrip
/** Callback impl that calls directly into the cxx bridge. Created from C++. */
@DoNotStrip
public class CxxCallbackImpl @DoNotStrip private constructor() : HybridClassBase(), Callback {
override fun invoke(vararg args: Any?) {
@Suppress("UNCHECKED_CAST") nativeInvoke(Arguments.fromJavaArgs(args as Array<Any?>))
}
private external fun nativeInvoke(arguments: NativeArray)
}

View File

@@ -0,0 +1,21 @@
/*
* 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.bridge
/** Crashy crashy exception handler. */
public class DefaultJSExceptionHandler : JSExceptionHandler {
override fun handleException(e: Exception) {
throw if (e is RuntimeException) {
// Because we are rethrowing the original exception, the original stacktrace will be
// preserved.
e
} else {
RuntimeException(e)
}
}
}

View File

@@ -0,0 +1,28 @@
/*
* 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.bridge
import com.facebook.yoga.YogaUnit
import com.facebook.yoga.YogaValue
internal class DimensionPropConverter {
companion object {
@JvmStatic
fun getDimension(value: Any?): YogaValue? {
return when (value) {
null -> null
is Double -> YogaValue(value.toFloat(), YogaUnit.POINT)
is String -> YogaValue.parse(value)
else ->
throw JSApplicationCausedNativeException(
"DimensionValue: the value must be a number or string."
)
}
}
}
}

View File

@@ -0,0 +1,32 @@
/*
* 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.bridge
/**
* Type representing a piece of data with unknown runtime type. Useful for allowing javascript to
* pass one of multiple types down to the native layer.
*/
public interface Dynamic {
public val type: ReadableType
public val isNull: Boolean
public fun asArray(): ReadableArray?
public fun asBoolean(): Boolean
public fun asDouble(): Double
public fun asInt(): Int
public fun asMap(): ReadableMap?
public fun asString(): String?
public fun recycle()
}

View File

@@ -0,0 +1,66 @@
/*
* 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.bridge
import androidx.core.util.Pools.SimplePool
/** Implementation of Dynamic wrapping a ReadableArray. */
internal class DynamicFromArray private constructor() : Dynamic {
private var array: ReadableArray? = null
private var index: Int = -1
override fun recycle() {
array = null
index = -1
pool.get()?.release(this)
}
override val type: ReadableType
get() =
array?.getType(index) ?: throw IllegalStateException("This dynamic value has been recycled")
override val isNull: Boolean
get() =
array?.isNull(index) ?: throw IllegalStateException("This dynamic value has been recycled")
override fun asArray(): ReadableArray =
array?.getArray(index) ?: throw IllegalStateException("This dynamic value has been recycled")
override fun asBoolean(): Boolean =
array?.getBoolean(index)
?: throw IllegalStateException("This dynamic value has been recycled")
override fun asDouble(): Double =
array?.getDouble(index) ?: throw IllegalStateException("This dynamic value has been recycled")
override fun asInt(): Int =
array?.getInt(index) ?: throw IllegalStateException("This dynamic value has been recycled")
override fun asMap(): ReadableMap =
array?.getMap(index) ?: throw IllegalStateException("This dynamic value has been recycled")
override fun asString(): String =
array?.getString(index) ?: throw IllegalStateException("This dynamic value has been recycled")
companion object {
private val pool: ThreadLocal<SimplePool<DynamicFromArray>> =
object : ThreadLocal<SimplePool<DynamicFromArray>>() {
override fun initialValue(): SimplePool<DynamicFromArray> {
return SimplePool(10)
}
}
@JvmStatic
fun create(array: ReadableArray, index: Int): DynamicFromArray {
val dynamic = pool.get()?.acquire() ?: DynamicFromArray()
dynamic.array = array
dynamic.index = index
return dynamic
}
}
}

View File

@@ -0,0 +1,80 @@
/*
* 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.bridge
import androidx.core.util.Pools.SimplePool
/** Implementation of Dynamic wrapping a ReadableMap. */
internal class DynamicFromMap
// This is a pools object. Hide the constructor.
private constructor() : Dynamic {
private var map: ReadableMap? = null
private var name: String? = null
override fun recycle() {
map = null
name = null
pool.get()?.release(this)
}
override val isNull: Boolean
get() {
return accessMapSafely { map, name -> map.isNull(name) }
}
override fun asBoolean(): Boolean = accessMapSafely { map, name -> map.getBoolean(name) }
override fun asDouble(): Double = accessMapSafely { map, name -> map.getDouble(name) }
override fun asInt(): Int = accessMapSafely { map, name -> map.getInt(name) }
override fun asString(): String? = accessMapSafely { map, name -> map.getString(name) }
override fun asArray(): ReadableArray? = accessMapSafely { map, name -> map.getArray(name) }
override fun asMap(): ReadableMap? = accessMapSafely { map, name -> map.getMap(name) }
override val type: ReadableType
get() {
return accessMapSafely { map, name -> map.getType(name) }
}
/**
* Asserts that both map and name are non-null and invokes the lambda with
*
* @param executor the callback to be invoked with non-null-asserted prop values
* @return value returned by the executor
*/
private fun <T> accessMapSafely(executor: (map: ReadableMap, name: String) -> T): T {
val name = checkNotNull(name) { DYNAMIC_VALUE_RECYCLED_FAILURE_MESSAGE }
val map = checkNotNull(map) { DYNAMIC_VALUE_RECYCLED_FAILURE_MESSAGE }
return executor(map, name)
}
companion object {
private val pool: ThreadLocal<SimplePool<DynamicFromMap>> =
object : ThreadLocal<SimplePool<DynamicFromMap>>() {
override fun initialValue(): SimplePool<DynamicFromMap> {
return SimplePool(10)
}
}
private const val DYNAMIC_VALUE_RECYCLED_FAILURE_MESSAGE =
"This dynamic value has been recycled"
fun create(map: ReadableMap, name: String): DynamicFromMap {
val dynamic = pool.get()?.acquire() ?: DynamicFromMap()
return dynamic.apply {
this.map = map
this.name = name
}
}
}
}

View File

@@ -0,0 +1,80 @@
/*
* 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.bridge
import com.facebook.common.logging.FLog
import com.facebook.react.common.ReactConstants
/** Implementation of Dynamic wrapping a ReadableArray. */
public class DynamicFromObject(private val value: Any?) : Dynamic {
override fun recycle() {
// Noop - nothing to recycle since there is no pooling
}
override val isNull: Boolean
get() = value == null
override fun asBoolean(): Boolean {
if (value is Boolean) {
return value
}
throw ClassCastException("Dynamic value from Object is not a boolean")
}
override fun asDouble(): Double {
if (value is Number) {
return value as Double
}
throw ClassCastException("Dynamic value from Object is not a number")
}
override fun asInt(): Int {
if (value is Number) {
// Numbers from JS are always Doubles
return (value as Double).toInt()
}
throw ClassCastException("Dynamic value from Object is not a number")
}
override fun asString(): String {
if (value is String) {
return value
}
throw ClassCastException("Dynamic value from Object is not a string")
}
override fun asArray(): ReadableArray {
if (value is ReadableArray) {
return value
}
throw ClassCastException("Dynamic value from Object is not a ReadableArray")
}
override fun asMap(): ReadableMap {
if (value is ReadableMap) {
return value
}
throw ClassCastException("Dynamic value from Object is not a ReadableMap")
}
override val type: ReadableType
get() {
return when (value) {
null -> ReadableType.Null
is Boolean -> ReadableType.Boolean
is Number -> ReadableType.Number
is String -> ReadableType.String
is ReadableMap -> ReadableType.Map
is ReadableArray -> ReadableType.Array
else -> {
FLog.e(ReactConstants.TAG, "Unmapped object type " + (value.javaClass.name))
ReadableType.Null
}
}
}
}

View File

@@ -0,0 +1,54 @@
/*
* 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.bridge
import com.facebook.jni.HybridClassBase
import com.facebook.proguard.annotations.DoNotStripAny
/**
* An implementation of [Dynamic] that has a C++ implementation.
*
* This is used to support Legacy Native Modules that have not been migrated to the new architecture
* and are using [Dynamic] as a parameter type.
*/
@DoNotStripAny
private class DynamicNative : HybridClassBase(), Dynamic {
override val type: ReadableType
get() = getTypeNative()
override val isNull: Boolean
get() = isNullNative()
private external fun getTypeNative(): ReadableType
private external fun isNullNative(): Boolean
external override fun asBoolean(): Boolean
// The native representation is holding the value as Double. We do the Int conversion here.
override fun asInt(): Int = asDouble().toInt()
external override fun asDouble(): Double
external override fun asString(): String
external override fun asArray(): ReadableArray
external override fun asMap(): ReadableMap
override fun recycle() {
// Noop - nothing to recycle since there is no pooling
}
private companion object {
init {
ReactNativeJniCommonSoLoader.staticInit()
}
}
}

View File

@@ -0,0 +1,42 @@
/*
* 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.
*/
@file:Suppress("DEPRECATION") // Need to migrate away from AsyncTasks
package com.facebook.react.bridge
import android.os.AsyncTask
import java.util.concurrent.Executor
/**
* Abstract base for a AsyncTask that should have any RuntimeExceptions it throws handled by the
* [JSExceptionHandler] registered if the app is in dev mode.
*
* This class doesn't allow doInBackground to return a results. If you need this use
* GuardedResultAsyncTask instead.
*/
public abstract class GuardedAsyncTask<Params, Progress>
protected constructor(private val exceptionHandler: JSExceptionHandler) :
AsyncTask<Params, Progress, Void>() {
protected constructor(reactContext: ReactContext) : this(reactContext.exceptionHandler)
@Deprecated("AsyncTask is deprecated.")
protected final override fun doInBackground(vararg params: Params): Void? {
try {
doInBackgroundGuarded(*params)
} catch (e: RuntimeException) {
exceptionHandler.handleException(e)
}
return null
}
protected abstract fun doInBackgroundGuarded(vararg params: Params)
public companion object {
@JvmField public val THREAD_POOL_EXECUTOR: Executor = AsyncTask.THREAD_POOL_EXECUTOR
}
}

View File

@@ -0,0 +1,26 @@
/*
* 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.bridge
/**
* Abstract base for a Runnable that should have any RuntimeExceptions it throws handled by the
* [JSExceptionHandler] registered if the app is in dev mode.
*/
public abstract class GuardedRunnable(private val exceptionHandler: JSExceptionHandler) : Runnable {
public constructor(reactContext: ReactContext) : this(reactContext.exceptionHandler)
final override fun run() {
try {
runGuarded()
} catch (e: RuntimeException) {
exceptionHandler.handleException(e)
}
}
public abstract fun runGuarded()
}

View File

@@ -0,0 +1,34 @@
/*
* 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.bridge
/**
* A special RuntimeException that should be thrown by native code if it has reached an exceptional
* state due to a, or a sequence of, bad commands.
*
* <p>A good rule of thumb for whether a native Exception should extend this interface is 1) Can a
* developer make a change or correction in JS to keep this Exception from being thrown? 2) Is the
* app outside of this catalyst instance still in a good state to allow reloading and restarting
* this catalyst instance?
*
* <p>Examples where this class is appropriate to throw:
* <ul>
* <li>JS tries to update a view with a tag that hasn't been created yet
* <li>JS tries to show a static image that isn't in resources
* <li>JS tries to use an unsupported view class
* </ul>
*
* <p>Examples where this class **isn't** appropriate to throw: - Failed to write to localStorage
* because disk is full - Assertions about internal state (e.g. that
* child.getParent().indexOf(child) != -1)
*/
public open class JSApplicationCausedNativeException : RuntimeException {
public constructor(detailMessage: String) : super(detailMessage)
public constructor(detailMessage: String, throwable: Throwable?) : super(detailMessage, throwable)
}

View File

@@ -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.bridge
/** An illegal argument Exception caused by an argument passed from JS. */
public class JSApplicationIllegalArgumentException : JSApplicationCausedNativeException {
public constructor(detailMessage: String) : super(detailMessage)
public constructor(detailMessage: String, t: Throwable) : super(detailMessage, t)
}

View File

@@ -0,0 +1,99 @@
/*
* 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.bridge
import android.content.Context
import com.facebook.react.common.DebugServerException
/** A class that stores JS bundle information and allows a [JSBundleLoaderDelegate]. */
public abstract class JSBundleLoader {
/** Loads the script, returning the URL of the source it loaded. */
public abstract fun loadScript(delegate: JSBundleLoaderDelegate): String
public companion object {
/**
* This loader is recommended one for release version of your app. In that case local JS
* executor should be used. JS bundle will be read from assets in native code to save on passing
* large strings from java to native memory.
*/
@JvmStatic
public fun createAssetLoader(
context: Context,
assetUrl: String,
loadSynchronously: Boolean,
): JSBundleLoader =
object : JSBundleLoader() {
override fun loadScript(delegate: JSBundleLoaderDelegate): String {
delegate.loadScriptFromAssets(context.assets, assetUrl, loadSynchronously)
return assetUrl
}
}
/**
* This loader loads bundle from file system. The bundle will be read in native code to save on
* passing large strings from java to native memory.
*/
@JvmStatic
public fun createFileLoader(fileName: String): JSBundleLoader =
createFileLoader(fileName, fileName, false)
@JvmStatic
public fun createFileLoader(
fileName: String,
assetUrl: String,
loadSynchronously: Boolean,
): JSBundleLoader =
object : JSBundleLoader() {
override fun loadScript(delegate: JSBundleLoaderDelegate): String {
delegate.loadScriptFromFile(fileName, assetUrl, loadSynchronously)
return fileName
}
}
/**
* This loader is used when bundle gets reloaded from dev server. In that case loader expect JS
* bundle to be prefetched and stored in local file. We do that to avoid passing large strings
* between java and native code and avoid allocating memory in java to fit whole JS bundle in
* it. Providing correct [sourceURL] of downloaded bundle is required for JS stacktraces to work
* correctly and allows for source maps to correctly symbolize those.
*/
@JvmStatic
public fun createCachedBundleFromNetworkLoader(
sourceURL: String,
cachedFileLocation: String,
): JSBundleLoader =
object : JSBundleLoader() {
override fun loadScript(delegate: JSBundleLoaderDelegate): String {
return try {
delegate.loadScriptFromFile(cachedFileLocation, sourceURL, false)
sourceURL
} catch (e: Exception) {
throw DebugServerException.makeGeneric(sourceURL, e.message.orEmpty(), e)
}
}
}
/** Same as [createCachedBundleFromNetworkLoader], but for split bundles in development. */
@JvmStatic
public fun createCachedSplitBundleFromNetworkLoader(
sourceURL: String,
cachedFileLocation: String,
): JSBundleLoader =
object : JSBundleLoader() {
override fun loadScript(delegate: JSBundleLoaderDelegate): String {
return try {
delegate.loadSplitBundleFromFile(cachedFileLocation, sourceURL)
sourceURL
} catch (e: Exception) {
throw DebugServerException.makeGeneric(sourceURL, e.message.orEmpty(), e)
}
}
}
}
}

View File

@@ -0,0 +1,52 @@
/*
* 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.bridge
import android.content.res.AssetManager
/** An interface for classes that initialize JavaScript using [JSBundleLoader] */
public interface JSBundleLoaderDelegate {
/**
* Load a JS bundle from Android assets. See [JSBundleLoader.createAssetLoader]
*
* @param assetManager
* @param assetURL
* @param loadSynchronously
*/
public fun loadScriptFromAssets(
assetManager: AssetManager,
assetURL: String,
loadSynchronously: Boolean,
)
/**
* Load a JS bundle from the filesystem. See [JSBundleLoader.createFileLoader] and
* [JSBundleLoader.createCachedBundleFromNetworkLoader]
*
* @param fileName
* @param sourceURL
* @param loadSynchronously
*/
public fun loadScriptFromFile(fileName: String, sourceURL: String, loadSynchronously: Boolean)
/**
* Load a split JS bundle from the filesystem. See
* [JSBundleLoader.createCachedSplitBundleFromNetworkLoader].
*/
public fun loadSplitBundleFromFile(fileName: String, sourceURL: String)
/**
* This API is used in situations where the JS bundle is being executed not on the device, but on
* a host machine. In that case, we must provide two source URLs for the JS bundle: One to be used
* on the device, and one to be used on the remote debugging machine.
*
* @param deviceURL A source URL that is accessible from this device.
* @param remoteURL A source URL that is accessible from the remote machine executing the JS.
*/
public fun setSourceURLs(deviceURL: String, remoteURL: String)
}

View File

@@ -0,0 +1,22 @@
/*
* 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.bridge
/**
* Interface for a class that knows how to handle an Exception invoked from JS. Since these
* Exceptions are triggered by JS calls (and can be fixed in JS), a common way to handle one is to
* show a error dialog and allow the developer to change and reload JS.
*
* We should also note that we have a unique stance on what 'caused' means: even if there's a bug in
* the framework/native code, it was triggered by JS and theoretically since we were able to set up
* the React Instance, JS could change its logic, reload, and not trigger that crash.
*/
public fun interface JSExceptionHandler {
/** Do something to display or log the exception. */
public fun handleException(e: Exception)
}

View File

@@ -0,0 +1,22 @@
/*
* 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.bridge
import com.facebook.react.common.annotations.internal.InteropLegacyArchitecture
/**
* This interface includes the methods needed to use a running JS instance, without specifying any
* of the bridge-specific initialization or lifecycle management.
*/
@InteropLegacyArchitecture
public interface JSInstance {
public fun invokeCallback(callbackID: Int, arguments: NativeArrayInterface)
// TODO if this interface survives refactoring, think about adding
// callFunction.
}

View File

@@ -0,0 +1,442 @@
/*
* 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.bridge
import com.facebook.debug.holder.PrinterHolder
import com.facebook.debug.tags.ReactDebugOverlayTags
import com.facebook.react.common.annotations.internal.LegacyArchitecture
import com.facebook.react.common.annotations.internal.LegacyArchitectureLogLevel
import com.facebook.react.common.annotations.internal.LegacyArchitectureLogger
import com.facebook.systrace.Systrace.TRACE_TAG_REACT
import com.facebook.systrace.SystraceMessage
import java.lang.reflect.InvocationTargetException
import java.lang.reflect.Method
@Deprecated(
message = "This class is part of Legacy Architecture and will be removed in a future release",
level = DeprecationLevel.WARNING,
)
@LegacyArchitecture(logLevel = LegacyArchitectureLogLevel.ERROR)
internal class JavaMethodWrapper(
private val moduleWrapper: JavaModuleWrapper,
val method: Method,
isSync: Boolean,
) : JavaModuleWrapper.NativeMethod {
private abstract class ArgumentExtractor<T> {
open fun getJSArgumentsNeeded(): Int = 1
@Suppress("DEPRECATION")
abstract fun extractArgument(
jsInstance: JSInstance,
jsArguments: ReadableArray,
atIndex: Int,
): T?
}
private val parameterTypes: Array<Class<*>>
private val paramLength: Int
/**
* Determines how the method is exported in JavaScript: METHOD_TYPE_ASYNC for regular methods
* METHOD_TYPE_PROMISE for methods that return a promise object to the caller. METHOD_TYPE_SYNC
* for sync methods
*/
override var type: String = BaseJavaModule.METHOD_TYPE_ASYNC
private var argumentsProcessed = false
private var argumentExtractors: Array<ArgumentExtractor<*>>? = null
private var internalSignature: String? = null
private var arguments: Array<Any?>? = null
private var jsArgumentsNeeded = 0
val signature: String?
get() {
if (!argumentsProcessed) {
processArguments()
}
return checkNotNull(internalSignature)
}
init {
method.isAccessible = true
parameterTypes = method.parameterTypes
paramLength = parameterTypes.size
if (isSync) {
type = BaseJavaModule.METHOD_TYPE_SYNC
} else if (paramLength > 0 && (parameterTypes[paramLength - 1] == Promise::class.java)) {
type = BaseJavaModule.METHOD_TYPE_PROMISE
}
}
private fun processArguments() {
if (argumentsProcessed) {
return
}
SystraceMessage.beginSection(TRACE_TAG_REACT, "processArguments")
.arg("method", moduleWrapper.name + "." + method.name)
.flush()
try {
argumentsProcessed = true
argumentExtractors = buildArgumentExtractors(parameterTypes)
internalSignature =
buildSignature(method, parameterTypes, (type == BaseJavaModule.METHOD_TYPE_SYNC))
// Since native methods are invoked from a message queue executed on a single thread, it is
// safe to allocate only one arguments object per method that can be reused across calls
arguments = arrayOfNulls(parameterTypes.size)
jsArgumentsNeeded = calculateJSArgumentsNeeded()
} finally {
SystraceMessage.endSection(TRACE_TAG_REACT).flush()
}
}
private fun buildSignature(method: Method, paramTypes: Array<Class<*>>, isSync: Boolean): String =
buildString(paramTypes.size + 2) {
if (isSync) {
append(returnTypeToChar(method.returnType))
append('.')
} else {
append("v.")
}
for (i in paramTypes.indices) {
val paramClass = paramTypes[i]
if (paramClass == Promise::class.java) {
check(i == paramTypes.size - 1) { "Promise must be used as last parameter only" }
}
append(paramTypeToChar(paramClass))
}
}
private fun buildArgumentExtractors(paramTypes: Array<Class<*>>): Array<ArgumentExtractor<*>> {
val argumentExtractors = arrayOfNulls<ArgumentExtractor<*>>(paramTypes.size)
var i = 0
while (i < paramTypes.size) {
val argumentClass = paramTypes[i]
val extractor: ArgumentExtractor<*> =
when (argumentClass) {
Boolean::class.javaObjectType,
Boolean::class.javaPrimitiveType -> ARGUMENT_EXTRACTOR_BOOLEAN
Int::class.javaObjectType,
Int::class.javaPrimitiveType -> ARGUMENT_EXTRACTOR_INTEGER
Double::class.javaObjectType,
Double::class.javaPrimitiveType -> ARGUMENT_EXTRACTOR_DOUBLE
Float::class.javaObjectType,
Float::class.javaPrimitiveType -> ARGUMENT_EXTRACTOR_FLOAT
String::class.java -> ARGUMENT_EXTRACTOR_STRING
Callback::class.java -> ARGUMENT_EXTRACTOR_CALLBACK
Promise::class.java -> {
check(i == paramTypes.size - 1) { "Promise must be used as last parameter only" }
ARGUMENT_EXTRACTOR_PROMISE
}
ReadableMap::class.java -> ARGUMENT_EXTRACTOR_MAP
ReadableArray::class.java -> ARGUMENT_EXTRACTOR_ARRAY
Dynamic::class.java -> ARGUMENT_EXTRACTOR_DYNAMIC
else ->
throw RuntimeException("Got unknown argument class: ${argumentClass.simpleName}")
}
argumentExtractors[i] = extractor
i += extractor.getJSArgumentsNeeded()
}
return argumentExtractors.requireNoNulls()
}
private fun calculateJSArgumentsNeeded(): Int {
var n = 0
for (extractor in checkNotNull(argumentExtractors)) {
n += extractor.getJSArgumentsNeeded()
}
return n
}
private fun getAffectedRange(startIndex: Int, jsArgumentsNeeded: Int): String =
if (jsArgumentsNeeded > 1) {
"$startIndex-${startIndex + jsArgumentsNeeded - 1}"
} else {
"$startIndex"
}
@Suppress("DEPRECATION")
override fun invoke(jsInstance: JSInstance, parameters: ReadableArray) {
val traceName = moduleWrapper.name + "." + method.name
SystraceMessage.beginSection(TRACE_TAG_REACT, "callJavaModuleMethod")
.arg("method", traceName)
.flush()
if (DEBUG) {
PrinterHolder.printer.logMessage(
ReactDebugOverlayTags.BRIDGE_CALLS,
"JS->Java: %s.%s()",
moduleWrapper.name,
method.name,
)
}
try {
if (!argumentsProcessed) {
processArguments()
}
val validatedArguments =
requireNotNull(arguments) { "processArguments failed: 'arguments' is null." }
val validatedArgumentExtractors =
requireNotNull(argumentExtractors) {
"processArguments failed: 'argumentExtractors' is null."
}
if (jsArgumentsNeeded != parameters.size()) {
throw JSApplicationCausedNativeException(
"$traceName got ${parameters.size()} arguments, expected $jsArgumentsNeeded"
)
}
var i = 0
var jsArgumentsConsumed = 0
try {
while (i < validatedArgumentExtractors.size) {
validatedArguments[i] =
validatedArgumentExtractors[i].extractArgument(
jsInstance,
parameters,
jsArgumentsConsumed,
)
jsArgumentsConsumed += validatedArgumentExtractors[i].getJSArgumentsNeeded()
i++
}
} catch (e: UnexpectedNativeTypeException) {
throw JSApplicationCausedNativeException(
"${e.message} (constructing arguments for $traceName at argument index ${
getAffectedRange(
jsArgumentsConsumed,
validatedArgumentExtractors[i].getJSArgumentsNeeded(),
)
})",
e,
)
} catch (e: NullPointerException) {
throw JSApplicationCausedNativeException(
"${e.message} (constructing arguments for $traceName at argument index ${
getAffectedRange(
jsArgumentsConsumed,
validatedArgumentExtractors[i].getJSArgumentsNeeded(),
)
})",
e,
)
}
try {
method.invoke(moduleWrapper.module, *validatedArguments)
} catch (e: IllegalArgumentException) {
throw RuntimeException(createInvokeExceptionMessage(traceName), e)
} catch (e: IllegalAccessException) {
throw RuntimeException(createInvokeExceptionMessage(traceName), e)
} catch (e: InvocationTargetException) {
// Exceptions thrown from native module calls end up wrapped in InvocationTargetException
// which just make traces harder to read and bump out useful information
if (e.cause is RuntimeException) {
throw (e.cause as RuntimeException)
}
throw RuntimeException(createInvokeExceptionMessage(traceName), e)
}
} finally {
SystraceMessage.endSection(TRACE_TAG_REACT).flush()
}
}
companion object {
private val ARGUMENT_EXTRACTOR_BOOLEAN: ArgumentExtractor<Boolean> =
object : ArgumentExtractor<Boolean>() {
@Suppress("DEPRECATION")
override fun extractArgument(
jsInstance: JSInstance,
jsArguments: ReadableArray,
atIndex: Int,
): Boolean = jsArguments.getBoolean(atIndex)
}
private val ARGUMENT_EXTRACTOR_DOUBLE: ArgumentExtractor<Double> =
object : ArgumentExtractor<Double>() {
@Suppress("DEPRECATION")
override fun extractArgument(
jsInstance: JSInstance,
jsArguments: ReadableArray,
atIndex: Int,
): Double = jsArguments.getDouble(atIndex)
}
private val ARGUMENT_EXTRACTOR_FLOAT: ArgumentExtractor<Float> =
object : ArgumentExtractor<Float>() {
@Suppress("DEPRECATION")
override fun extractArgument(
jsInstance: JSInstance,
jsArguments: ReadableArray,
atIndex: Int,
): Float = jsArguments.getDouble(atIndex).toFloat()
}
private val ARGUMENT_EXTRACTOR_INTEGER: ArgumentExtractor<Int> =
object : ArgumentExtractor<Int>() {
@Suppress("DEPRECATION")
override fun extractArgument(
jsInstance: JSInstance,
jsArguments: ReadableArray,
atIndex: Int,
): Int = jsArguments.getDouble(atIndex).toInt()
}
private val ARGUMENT_EXTRACTOR_STRING: ArgumentExtractor<String> =
object : ArgumentExtractor<String>() {
@Suppress("DEPRECATION")
override fun extractArgument(
jsInstance: JSInstance,
jsArguments: ReadableArray,
atIndex: Int,
): String? = jsArguments.getString(atIndex)
}
private val ARGUMENT_EXTRACTOR_ARRAY: ArgumentExtractor<ReadableArray> =
object : ArgumentExtractor<ReadableArray>() {
@Suppress("DEPRECATION")
override fun extractArgument(
jsInstance: JSInstance,
jsArguments: ReadableArray,
atIndex: Int,
): ReadableArray? = jsArguments.getArray(atIndex)
}
private val ARGUMENT_EXTRACTOR_DYNAMIC: ArgumentExtractor<Dynamic> =
object : ArgumentExtractor<Dynamic>() {
@Suppress("DEPRECATION")
override fun extractArgument(
jsInstance: JSInstance,
jsArguments: ReadableArray,
atIndex: Int,
): Dynamic = DynamicFromArray.create(jsArguments, atIndex)
}
private val ARGUMENT_EXTRACTOR_MAP: ArgumentExtractor<ReadableMap> =
object : ArgumentExtractor<ReadableMap>() {
@Suppress("DEPRECATION")
override fun extractArgument(
jsInstance: JSInstance,
jsArguments: ReadableArray,
atIndex: Int,
): ReadableMap? = jsArguments.getMap(atIndex)
}
private val ARGUMENT_EXTRACTOR_CALLBACK: ArgumentExtractor<Callback> =
object : ArgumentExtractor<Callback>() {
@Suppress("DEPRECATION")
override fun extractArgument(
jsInstance: JSInstance,
jsArguments: ReadableArray,
atIndex: Int,
): Callback? =
if (jsArguments.isNull(atIndex)) {
null
} else {
object : Callback {
private var invoked = false
override fun invoke(vararg args: Any?) {
if (invoked) {
error(
"Illegal callback invocation from native module. This callback type only permits a single invocation from native code."
)
}
@Suppress("UNCHECKED_CAST")
jsInstance.invokeCallback(
callbackID = jsArguments.getDouble(atIndex).toInt(),
arguments = Arguments.fromJavaArgs(args as Array<Any?>),
)
invoked = true
}
}
}
}
private val ARGUMENT_EXTRACTOR_PROMISE: ArgumentExtractor<Promise> =
object : ArgumentExtractor<Promise>() {
override fun getJSArgumentsNeeded(): Int = 2
@Suppress("DEPRECATION")
override fun extractArgument(
jsInstance: JSInstance,
jsArguments: ReadableArray,
atIndex: Int,
): Promise {
val resolve =
ARGUMENT_EXTRACTOR_CALLBACK.extractArgument(jsInstance, jsArguments, atIndex)
val reject =
ARGUMENT_EXTRACTOR_CALLBACK.extractArgument(jsInstance, jsArguments, atIndex + 1)
return PromiseImpl(resolve, reject)
}
}
private val DEBUG =
PrinterHolder.printer.shouldDisplayLogMessage(ReactDebugOverlayTags.BRIDGE_CALLS)
init {
LegacyArchitectureLogger.assertLegacyArchitecture(
"JavaMethodWrapper",
LegacyArchitectureLogLevel.ERROR,
)
}
private fun paramTypeToChar(paramClass: Class<*>): Char {
val tryCommon = commonTypeToChar(paramClass)
if (tryCommon != '\u0000') {
return tryCommon
}
return when (paramClass) {
Callback::class.java -> 'X'
Promise::class.java -> 'P'
ReadableMap::class.java -> 'M'
ReadableArray::class.java -> 'A'
Dynamic::class.java -> 'Y'
else -> throw RuntimeException("Got unknown param class: ${paramClass.simpleName}")
}
}
private fun returnTypeToChar(returnClass: Class<*>): Char {
// Keep this in sync with MethodInvoker
val tryCommon = commonTypeToChar(returnClass)
if (tryCommon != '\u0000') {
return tryCommon
}
return when (returnClass) {
Void.TYPE -> 'v'
WritableMap::class.java -> 'M'
WritableArray::class.java -> 'A'
else -> throw RuntimeException("Got unknown return class: ${returnClass.simpleName}")
}
}
private fun commonTypeToChar(typeClass: Class<*>): Char {
return when (typeClass) {
Boolean::class.javaPrimitiveType -> 'z'
Boolean::class.javaObjectType -> 'Z'
Int::class.javaPrimitiveType -> 'i'
Int::class.javaObjectType -> 'I'
Double::class.javaPrimitiveType -> 'd'
Double::class.javaObjectType -> 'D'
Float::class.javaPrimitiveType -> 'f'
Float::class.javaObjectType -> 'F'
String::class.java -> 'S'
else -> '\u0000'
}
}
/**
* Makes it easier to determine the cause of an error invoking a native method from Javascript
* code by adding the function name.
*/
private fun createInvokeExceptionMessage(traceName: String): String =
"Could not invoke $traceName"
}
}

View File

@@ -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.bridge
import com.facebook.proguard.annotations.DoNotStrip
import com.facebook.react.common.annotations.internal.InteropLegacyArchitecture
import com.facebook.react.common.annotations.internal.LegacyArchitectureLogLevel
import com.facebook.react.common.annotations.internal.LegacyArchitectureLogger.assertLegacyArchitecture
import com.facebook.react.turbomodule.core.interfaces.TurboModule
import com.facebook.systrace.Systrace
import com.facebook.systrace.Systrace.TRACE_TAG_REACT
import com.facebook.systrace.SystraceMessage
import java.lang.reflect.Method
/**
* This is part of the glue which wraps a java BaseJavaModule in a C++ NativeModule. This could all
* be in C++, but it's android-specific initialization code, and writing it this way is easier to
* read and means fewer JNI calls.
*/
@DoNotStrip
@InteropLegacyArchitecture
internal class JavaModuleWrapper(
@Suppress("DEPRECATION") private val jsInstance: JSInstance,
private val moduleHolder: ModuleHolder,
) {
interface NativeMethod {
@Suppress("DEPRECATION") fun invoke(jsInstance: JSInstance, parameters: ReadableArray)
val type: String
}
@DoNotStrip
class MethodDescriptor {
@DoNotStrip var method: Method? = null
@DoNotStrip var signature: String? = null
@DoNotStrip var name: String? = null
@DoNotStrip var type: String? = null
}
private val methods = ArrayList<NativeMethod>()
private val descs = ArrayList<MethodDescriptor>()
@get:DoNotStrip
val module: BaseJavaModule
get() = moduleHolder.module as BaseJavaModule
@get:DoNotStrip
val name: String
get() = moduleHolder.name
@DoNotStrip
private fun findMethods() {
Systrace.beginSection(TRACE_TAG_REACT, "findMethods")
var classForMethods: Class<*> = moduleHolder.module.javaClass
val superClass = classForMethods.superclass
if (superClass != null && TurboModule::class.java.isAssignableFrom(superClass)) {
// For java module that is based on generated flow-type spec, inspect the
// spec abstract class instead, which is the super class of the given Java
// module.
classForMethods = superClass
}
val targetMethods = classForMethods.declaredMethods
for (targetMethod in targetMethods) {
targetMethod.getAnnotation(ReactMethod::class.java)?.let { annotation ->
val methodName = targetMethod.name
val md = MethodDescriptor()
@Suppress("DEPRECATION")
val method = JavaMethodWrapper(this, targetMethod, annotation.isBlockingSynchronousMethod)
md.name = methodName
md.type = method.type
if (BaseJavaModule.METHOD_TYPE_SYNC == md.type) {
md.signature = method.signature
md.method = targetMethod
}
methods.add(method)
descs.add(md)
}
}
Systrace.endSection(TRACE_TAG_REACT)
}
@get:DoNotStrip
val methodDescriptors: List<MethodDescriptor>
get() {
if (descs.isEmpty()) {
findMethods()
}
return descs
}
@get:DoNotStrip
val constants: NativeMap
get() {
val moduleName = name
SystraceMessage.beginSection(TRACE_TAG_REACT, "JavaModuleWrapper.getConstants")
.arg("moduleName", moduleName)
.flush()
ReactMarker.logMarker(ReactMarkerConstants.GET_CONSTANTS_START, moduleName)
val baseJavaModule = module
Systrace.beginSection(TRACE_TAG_REACT, "module.getConstants")
val map = baseJavaModule.constants
Systrace.endSection(TRACE_TAG_REACT)
Systrace.beginSection(TRACE_TAG_REACT, "create WritableNativeMap")
ReactMarker.logMarker(ReactMarkerConstants.CONVERT_CONSTANTS_START, moduleName)
try {
return Arguments.makeNativeMap(map)
} finally {
ReactMarker.logMarker(ReactMarkerConstants.CONVERT_CONSTANTS_END, moduleName)
Systrace.endSection(TRACE_TAG_REACT)
ReactMarker.logMarker(ReactMarkerConstants.GET_CONSTANTS_END, moduleName)
SystraceMessage.endSection(TRACE_TAG_REACT).flush()
}
}
@DoNotStrip
fun invoke(methodId: Int, parameters: ReadableNativeArray) {
if (methodId >= methods.size) {
return
}
methods[methodId].invoke(jsInstance, parameters)
}
private companion object {
init {
assertLegacyArchitecture("JavaModuleWrapper", LegacyArchitectureLogLevel.WARNING)
}
}
}

View File

@@ -0,0 +1,147 @@
/*
* 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.bridge
import com.facebook.react.bridge.JavaOnlyMap.Companion.deepClone
import java.util.ArrayList
/**
* Java [ArrayList] backed implementation of [ReadableArray] and [WritableArray] Instances of this
* class SHOULD NOT be used for communication between java and JS, use instances of
* [WritableNativeArray] created via [Arguments#createArray] or just [ReadableArray] interface if
* you want your "native" module method to take an array from JS as an argument.
*
* Main purpose for this class is to be used in java-only unit tests, but could also be used outside
* of tests in the code that operates only in java and needs to communicate with RN modules via
* their JS-exposed API.
*/
public class JavaOnlyArray : ReadableArray, WritableArray {
public companion object {
@JvmStatic public fun from(list: List<*>): JavaOnlyArray = JavaOnlyArray(list)
@JvmStatic public fun of(vararg values: Any?): JavaOnlyArray = JavaOnlyArray(*values)
@JvmStatic
public fun deepClone(array: ReadableArray?): JavaOnlyArray {
val res = JavaOnlyArray()
if (array == null) {
return res
}
repeat(array.size()) { i ->
val type = array.getType(i)
when (type) {
ReadableType.Null -> res.pushNull()
ReadableType.Boolean -> res.pushBoolean(array.getBoolean(i))
ReadableType.Number -> res.pushDouble(array.getDouble(i))
ReadableType.String -> res.pushString(array.getString(i))
ReadableType.Map -> res.pushMap(deepClone(array.getMap(i)))
ReadableType.Array -> res.pushArray(deepClone(array.getArray(i)))
}
}
return res
}
}
private val backingList: MutableList<Any?>
private constructor(vararg values: Any?) {
backingList = mutableListOf(*values)
}
private constructor(list: List<*>) {
backingList = ArrayList(list)
}
public constructor() {
backingList = mutableListOf()
}
override fun size(): Int = backingList.size
override fun isNull(index: Int): Boolean = backingList[index] == null
override fun getDouble(index: Int): Double = (backingList[index] as Number).toDouble()
override fun getInt(index: Int): Int = (backingList[index] as Number).toInt()
override fun getLong(index: Int): Long = (backingList[index] as Number).toLong()
override fun getString(index: Int): String? = backingList[index] as String?
override fun getArray(index: Int): ReadableArray? = backingList[index] as ReadableArray?
override fun getBoolean(index: Int): Boolean = backingList[index] as Boolean
override fun getMap(index: Int): ReadableMap? = backingList[index] as ReadableMap?
override fun getDynamic(index: Int): Dynamic = DynamicFromArray.create(this, index)
override fun getType(index: Int): ReadableType {
return when (val value = backingList[index]) {
null -> ReadableType.Null
is Boolean -> ReadableType.Boolean
is Double,
is Float,
is Int,
is Long -> ReadableType.Number
is String -> ReadableType.String
is ReadableArray -> ReadableType.Array
is ReadableMap -> ReadableType.Map
else -> throw IllegalStateException("Invalid type ${value.javaClass})")
}
}
override fun pushBoolean(value: Boolean) {
backingList.add(value)
}
override fun pushDouble(value: Double) {
backingList.add(value)
}
override fun pushInt(value: Int) {
backingList.add(value.toDouble())
}
override fun pushLong(value: Long) {
backingList.add(value.toDouble())
}
override fun pushString(value: String?) {
backingList.add(value)
}
override fun pushArray(array: ReadableArray?) {
backingList.add(array)
}
override fun pushMap(map: ReadableMap?) {
backingList.add(map)
}
override fun pushNull() {
backingList.add(null)
}
override fun toArrayList(): ArrayList<Any?> = ArrayList(backingList)
override fun toString(): String = backingList.toString()
override fun equals(other: Any?): Boolean =
if (this === other) {
true
} else if (other == null || javaClass != other.javaClass) {
false
} else {
backingList == (other as JavaOnlyArray).backingList
}
override fun hashCode(): Int = backingList.hashCode()
}

View File

@@ -0,0 +1,183 @@
/*
* 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.bridge
import java.util.HashMap
import kotlin.collections.Iterator
import kotlin.collections.Map
/**
* Java [HashMap] backed implementation of [ReadableMap] and [WritableMap] Instances of this class
* SHOULD NOT be used for communication between java and JS, use instances of [WritableNativeMap]
* created via [Arguments#createMap] or just [ReadableMap] interface if you want your "native"
* module method to take a map from JS as an argument.
*
* Main purpose for this class is to be used in java-only unit tests, but could also be used outside
* of tests in the code that operates only in java and needs to communicate with RN modules via
* their JS-exposed API.
*/
public class JavaOnlyMap() : ReadableMap, WritableMap {
public companion object {
@JvmStatic public fun of(vararg keysAndValues: Any?): JavaOnlyMap = JavaOnlyMap(*keysAndValues)
@JvmStatic public fun from(map: Map<String, Any?>): JavaOnlyMap = JavaOnlyMap(map)
@JvmStatic
public fun deepClone(map: ReadableMap?): JavaOnlyMap {
val res = JavaOnlyMap()
if (map == null) {
return res
}
val iter = map.keySetIterator()
while (iter.hasNextKey()) {
val propKey = iter.nextKey()
val type = map.getType(propKey)
when (type) {
ReadableType.Null -> res.putNull(propKey)
ReadableType.Boolean -> res.putBoolean(propKey, map.getBoolean(propKey))
ReadableType.Number -> res.putDouble(propKey, map.getDouble(propKey))
ReadableType.String -> res.putString(propKey, map.getString(propKey))
ReadableType.Map -> res.putMap(propKey, deepClone(map.getMap(propKey)))
ReadableType.Array ->
res.putArray(propKey, JavaOnlyArray.deepClone(map.getArray(propKey)))
}
}
return res
}
}
private val backingMap: MutableMap<String, Any?> = HashMap()
/** @param keysAndValues keys and values, interleaved */
private constructor(vararg keysAndValues: Any?) : this() {
require(keysAndValues.size % 2 == 0) { "You must provide the same number of keys and values" }
for (i in keysAndValues.indices step 2) {
var value = keysAndValues[i + 1]
if (value is Number) {
// all values from JS are doubles, so emulate that here for tests.
value = value.toDouble()
}
backingMap[keysAndValues[i] as String] = value
}
}
override fun hasKey(name: String): Boolean = backingMap.containsKey(name)
override fun isNull(name: String): Boolean = backingMap[name] == null
override fun getBoolean(name: String): Boolean = backingMap[name] as Boolean
override fun getDouble(name: String): Double = (backingMap[name] as Number).toDouble()
override fun getInt(name: String): Int = (backingMap[name] as Number).toInt()
override fun getLong(name: String): Long = (backingMap[name] as Number).toLong()
override fun getString(name: String): String? = backingMap[name] as String?
override fun getMap(name: String): ReadableMap? = backingMap[name] as ReadableMap?
override fun getArray(name: String): ReadableArray? = backingMap[name] as ReadableArray?
override fun getDynamic(name: String): Dynamic = DynamicFromMap.create(this, name)
override fun getType(name: String): ReadableType {
val value = backingMap[name]
return when {
value == null -> ReadableType.Null
value is Number -> ReadableType.Number
value is String -> ReadableType.String
value is Boolean -> ReadableType.Boolean
value is ReadableMap -> ReadableType.Map
value is ReadableArray -> ReadableType.Array
value is Dynamic -> value.type
else -> {
throw IllegalArgumentException(
"Invalid value $value for key $name contained in JavaOnlyMap"
)
}
}
}
override val entryIterator: Iterator<Map.Entry<String, Any?>>
get() = backingMap.entries.iterator()
override fun keySetIterator(): ReadableMapKeySetIterator {
return object : ReadableMapKeySetIterator {
private val iterator = backingMap.entries.iterator()
override fun hasNextKey(): Boolean = iterator.hasNext()
override fun nextKey(): String = iterator.next().key
}
}
override fun putBoolean(key: String, value: Boolean) {
backingMap[key] = value
}
override fun putDouble(key: String, value: Double) {
backingMap[key] = value
}
override fun putInt(key: String, value: Int) {
backingMap[key] = value.toDouble()
}
override fun putLong(key: String, value: Long) {
backingMap[key] = value.toDouble()
}
override fun putString(key: String, value: String?) {
backingMap[key] = value
}
override fun putNull(key: String) {
backingMap[key] = null
}
override fun putMap(key: String, value: ReadableMap?) {
backingMap[key] = value
}
override fun merge(source: ReadableMap) {
backingMap.putAll((source as JavaOnlyMap).backingMap)
}
override fun copy(): WritableMap {
val target = JavaOnlyMap()
target.merge(this)
return target
}
override fun putArray(key: String, value: ReadableArray?) {
backingMap[key] = value
}
public fun remove(key: String) {
backingMap.remove(key)
}
override fun toHashMap(): HashMap<String, Any?> = HashMap<String, Any?>(backingMap)
override fun toString(): String = backingMap.toString()
override fun equals(other: Any?): Boolean {
if (this === other) {
return true
}
if (other !is JavaOnlyMap) {
return false
}
val thisBackingMap = backingMap
val otherBackingMap = other.backingMap
return thisBackingMap == otherBackingMap
}
override fun hashCode(): Int = backingMap.hashCode()
}

View File

@@ -0,0 +1,26 @@
/*
* 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.bridge
import androidx.annotation.GuardedBy
/**
* Wrapper for JavaScriptContext native pointer. This object is creates on demand as part of the
* initialization of React native, and will call clear() before destroying the VM. People who need
* the raw JavaScriptContext pointer can synchronize on this wrapper object to guarantee that it
* will not be destroyed.
*/
public class JavaScriptContextHolder
public constructor(@field:GuardedBy("this") private var context: Long) {
@GuardedBy("this") public fun get(): Long = context
@Synchronized
public fun clear() {
context = 0
}
}

View File

@@ -0,0 +1,30 @@
/*
* 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.bridge
import com.facebook.jni.HybridData
import com.facebook.proguard.annotations.DoNotStripAny
@DoNotStripAny
public abstract class JavaScriptExecutor
protected constructor(
// fbjni looks for the exact name "mHybridData":
// https://github.com/facebookincubator/fbjni/blob/7b7efda0d49b956acf1d3307510e3c73fc55b404/cxx/fbjni/detail/Hybrid.h#L310
@Suppress("NoHungarianNotation") private val mHybridData: HybridData
) {
/**
* Close this executor and cleanup any resources that it was using. No further calls are expected
* after this. TODO mhorowitz: This may no longer be used; check and delete if possible.
*/
public open fun close() {
mHybridData.resetNative()
}
/** Returns the name of the executor, identifying the underlying runtime. */
public abstract fun getName(): String
}

View File

@@ -0,0 +1,25 @@
/*
* 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.bridge
public interface JavaScriptExecutorFactory {
@Throws(Exception::class) public fun create(): JavaScriptExecutor
/**
* Starts the sampling profiler for this specific JavaScriptExecutor Sampling profiler is usually
* a singleton on the runtime, hence the method exists here and not in [JavaScriptExecutor]
*/
public fun startSamplingProfiler()
/**
* Stops the Sampling profile
*
* @param filename The filename where the results of the sampling profiler are dumped to
*/
public fun stopSamplingProfiler(filename: String)
}

View File

@@ -0,0 +1,24 @@
/*
* 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.bridge
import com.facebook.proguard.annotations.DoNotStripAny
/**
* Interface denoting that a class is the interface to a module with the same name in JS. Calling
* functions on this interface will result in corresponding methods in JS being called.
*
* When extending JavaScriptModule and registering it with a CatalystInstance, all public methods
* are assumed to be implemented on a JS module with the same name as this class. Calling methods on
* the object returned from [ReactContext.getJSModule] or [CatalystInstance.getJSModule] will result
* in the methods with those names exported by that module being called in JS.
*
* NB: JavaScriptModule does not allow method name overloading because JS does not allow method name
* overloading.
*/
@DoNotStripAny public interface JavaScriptModule

View File

@@ -0,0 +1,95 @@
/*
* 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.bridge
import com.facebook.react.common.build.ReactBuildConfig
import java.lang.reflect.InvocationHandler
import java.lang.reflect.Method
import java.lang.reflect.Proxy
/**
* Class responsible for holding all the [JavaScriptModule]s. Uses Java proxy objects to dispatch
* method calls on JavaScriptModules to the bridge using the corresponding module and method ids so
* the proper function is executed in JavaScript.
*/
public class JavaScriptModuleRegistry {
private val moduleInstances: MutableMap<Class<out JavaScriptModule>, JavaScriptModule> = HashMap()
@Synchronized
public fun <T : JavaScriptModule> getJavaScriptModule(
@Suppress("DEPRECATION") instance: CatalystInstance,
moduleInterface: Class<T>,
): T {
val module = moduleInstances[moduleInterface]
if (module != null) {
@Suppress("UNCHECKED_CAST")
return module as T
}
val proxy =
Proxy.newProxyInstance(
moduleInterface.classLoader,
arrayOf<Class<*>>(moduleInterface),
JavaScriptModuleInvocationHandler(instance, moduleInterface),
) as JavaScriptModule
moduleInstances[moduleInterface] = proxy
@Suppress("UNCHECKED_CAST")
return proxy as T
}
private class JavaScriptModuleInvocationHandler(
@Suppress("DEPRECATION") private val catalystInstance: CatalystInstance,
private val moduleInterface: Class<out JavaScriptModule>,
) : InvocationHandler {
private var name: String? = null
init {
if (ReactBuildConfig.DEBUG) {
val methodNames = mutableSetOf<String>()
for (method in moduleInterface.declaredMethods) {
if (!methodNames.add(method.name)) {
throw AssertionError(
"Method overloading is unsupported: ${moduleInterface.name}#${method.name}"
)
}
}
}
}
private fun getJSModuleName(): String {
// Getting the class name every call is expensive, so cache it
return name ?: getJSModuleName(moduleInterface).also { name = it }
}
override fun invoke(proxy: Any, method: Method, args: Array<Any?>?): Any? {
val jsArgs = if (args != null) Arguments.fromJavaArgs(args) else WritableNativeArray()
catalystInstance.callFunction(getJSModuleName(), method.name, jsArgs)
return null
}
}
public companion object {
/**
* With proguard obfuscation turned on, proguard apparently (poorly) emulates inner classes or
* something because Class#getSimpleName() no longer strips the outer class name. We manually
* strip it here if necessary.
*/
@JvmStatic
public fun getJSModuleName(jsModuleInterface: Class<out JavaScriptModule>): String {
var name = jsModuleInterface.simpleName
val dollarSignIndex = name.lastIndexOf('$')
if (dollarSignIndex != -1) {
name = name.substring(dollarSignIndex + 1)
}
return name
}
}
}

View File

@@ -0,0 +1,129 @@
/*
* 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.bridge
import android.util.JsonWriter
import java.io.IOException
/** Helper for generating JSON for lists and maps. */
public object JsonWriterHelper {
@Throws(IOException::class)
@JvmStatic
public fun value(writer: JsonWriter, value: Any?) {
when (value) {
is Map<*, *> -> mapValue(writer, value)
is List<*> -> listValue(writer, value)
is ReadableMap -> readableMapValue(writer, value)
is ReadableArray -> readableArrayValue(writer, value)
is Dynamic -> dynamicValue(writer, value)
else -> objectValue(writer, value)
}
}
@Throws(IOException::class)
private fun dynamicValue(writer: JsonWriter, value: Dynamic) {
when (value.type) {
ReadableType.Null -> writer.nullValue()
ReadableType.Boolean -> writer.value(value.asBoolean())
ReadableType.Number -> writer.value(value.asDouble())
ReadableType.String -> writer.value(value.asString())
ReadableType.Map -> {
val map = value.asMap()
readableMapValue(writer, checkNotNull(map))
}
ReadableType.Array -> {
val array = value.asArray()
readableArrayValue(writer, checkNotNull(array))
}
}
}
@Throws(IOException::class)
private fun readableMapValue(writer: JsonWriter, value: ReadableMap) {
writer.beginObject()
try {
val iterator = value.keySetIterator()
while (iterator.hasNextKey()) {
val key = iterator.nextKey()
writer.name(key)
when (value.getType(key)) {
ReadableType.Null -> writer.nullValue()
ReadableType.Boolean -> writer.value(value.getBoolean(key))
ReadableType.Number -> writer.value(value.getDouble(key))
ReadableType.String -> writer.value(value.getString(key))
ReadableType.Map -> {
val map = value.getMap(key)
readableMapValue(writer, checkNotNull(map))
}
ReadableType.Array -> {
val array = value.getArray(key)
readableArrayValue(writer, checkNotNull(array))
}
}
}
} finally {
writer.endObject()
}
}
@Throws(IOException::class)
@JvmStatic
public fun readableArrayValue(writer: JsonWriter, value: ReadableArray) {
writer.beginArray()
try {
for (i in 0 until value.size()) {
when (value.getType(i)) {
ReadableType.Null -> writer.nullValue()
ReadableType.Boolean -> writer.value(value.getBoolean(i))
ReadableType.Number -> writer.value(value.getDouble(i))
ReadableType.String -> writer.value(value.getString(i))
ReadableType.Map -> {
val map = value.getMap(i)
readableMapValue(writer, checkNotNull(map))
}
ReadableType.Array -> {
val array = value.getArray(i)
readableArrayValue(writer, checkNotNull(array))
}
}
}
} finally {
writer.endArray()
}
}
@Throws(IOException::class)
private fun mapValue(writer: JsonWriter, map: Map<*, *>) {
writer.beginObject()
for ((key, value) in map) {
writer.name(key.toString())
this.value(writer, value)
}
writer.endObject()
}
@Throws(IOException::class)
private fun listValue(writer: JsonWriter, list: List<*>) {
writer.beginArray()
for (item in list) {
objectValue(writer, item)
}
writer.endArray()
}
@Throws(IOException::class)
private fun objectValue(writer: JsonWriter, value: Any?) {
when (value) {
null -> writer.nullValue()
is String -> writer.value(value)
is Number -> writer.value(value)
is Boolean -> writer.value(value)
else -> throw IllegalArgumentException("Unknown value: $value")
}
}
}

View File

@@ -0,0 +1,48 @@
/*
* 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.bridge
import android.app.Activity
/**
* Listener for receiving activity lifecycle events.
*
* When multiple activities share a react instance, only the most recent one's lifecycle events get
* forwarded to listeners. Consider the following scenarios:
* 1. Navigating from Activity A to B will trigger two events: A.onHostPause and B.onHostResume. Any
* subsequent lifecycle events coming from Activity A, such as onHostDestroy, will be ignored.
* 2. Navigating back from Activity B to Activity A will trigger the same events: B.onHostPause and
* A.onHostResume. Any subsequent events coming from Activity B, such as onHostDestroy, are
* ignored.
* 3. Navigating back from Activity A to a non-React Activity or to the home screen will trigger two
* events: onHostPause and onHostDestroy.
* 4. Navigating from Activity A to a non-React Activity B will trigger one event: onHostPause.
* Later, if Activity A is destroyed (e.g. because of resource contention), onHostDestroy is
* triggered.
*/
public interface LifecycleEventListener {
/**
* Called either when the host activity receives a resume event (e.g. [Activity.onResume] or if
* the native module that implements this is initialized while the host activity is already
* resumed. Always called for the most current activity.
*/
public fun onHostResume()
/**
* Called when host activity receives pause event (e.g. [Activity.onPause]. Always called for the
* most current activity.
*/
public fun onHostPause()
/**
* Called when host activity receives destroy event (e.g. [Activity.onDestroy]. Only called for
* the last React activity to be destroyed.
*/
public fun onHostDestroy()
}

View File

@@ -0,0 +1,14 @@
/*
* 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.bridge
/** Listener interface for memory pressure events. */
public fun interface MemoryPressureListener {
/** Called when the system generates a memory warning. */
public fun handleMemoryPressure(level: Int)
}

View File

@@ -0,0 +1,234 @@
/*
* 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.bridge
import androidx.annotation.GuardedBy
import com.facebook.common.logging.FLog
import com.facebook.debug.holder.PrinterHolder
import com.facebook.debug.tags.ReactDebugOverlayTags
import com.facebook.proguard.annotations.DoNotStrip
import com.facebook.react.common.ReactConstants
import com.facebook.react.module.model.ReactModuleInfo
import com.facebook.systrace.Systrace.TRACE_TAG_REACT
import com.facebook.systrace.SystraceMessage
import java.util.concurrent.atomic.AtomicInteger
import javax.inject.Provider
/**
* Holder to enable us to lazy create native modules.
*
* This works by taking a provider instead of an instance, when it is first required we'll create
* and initialize it. Initialization currently always happens on the UI thread but this is due to
* change for performance reasons.
*
* Lifecycle events via a [LifecycleEventListener] will still always happen on the UI thread.
*/
@DoNotStrip
public class ModuleHolder {
private val instanceKey = instanceKeyCounter.getAndIncrement()
@get:DoNotStrip public val name: String
private val reactModuleInfo: ReactModuleInfo
private var provider: Provider<out NativeModule>? = null
// Outside of the constructor, this should only be checked or set when synchronized on this
@GuardedBy("this") private var internalModule: NativeModule? = null
// This is used to communicate phases of creation and initialization across threads
@GuardedBy("this") private var initializable = false
@GuardedBy("this") private var isCreating = false
@GuardedBy("this") private var isInitializing = false
public constructor(moduleInfo: ReactModuleInfo, provider: Provider<out NativeModule?>) {
name = moduleInfo.name
this.provider = provider
reactModuleInfo = moduleInfo
if (moduleInfo.needsEagerInit) {
internalModule = create()
}
}
public constructor(nativeModule: NativeModule) {
name = nativeModule.name
@Suppress("DEPRECATION")
reactModuleInfo =
ReactModuleInfo(
nativeModule.name,
nativeModule.javaClass.simpleName,
nativeModule.canOverrideExistingModule(),
true,
false,
ReactModuleInfo.classIsTurboModule(nativeModule.javaClass),
)
internalModule = nativeModule
PrinterHolder.printer.logMessage(
ReactDebugOverlayTags.NATIVE_MODULE,
"NativeModule init: %s",
name,
)
}
/*
* Checks if [internalModule] has been created, and if so tries to initialize the module unless another
* thread is already doing the initialization.
* If [internalModule] has not been created, records that initialization is needed.
*/
internal fun markInitializable() {
var shouldInitializeNow = false
var module: NativeModule? = null
synchronized(this) {
initializable = true
if (internalModule != null) {
check(!isInitializing)
shouldInitializeNow = true
module = internalModule
}
}
if (shouldInitializeNow) {
checkNotNull(module)
doInitialize(module)
}
}
@Synchronized internal fun hasInstance(): Boolean = internalModule != null
@Synchronized
public fun destroy() {
internalModule?.invalidate()
}
public val canOverrideExistingModule: Boolean
get() = reactModuleInfo.canOverrideExistingModule
public val isTurboModule: Boolean
get() = reactModuleInfo.isTurboModule
public val isCxxModule: Boolean
get() = false
public val className: String
get() = reactModuleInfo.className
@get:DoNotStrip
public val module: NativeModule
get() {
val module: NativeModule
var shouldCreate = false
synchronized(this) {
val safeModule = internalModule
if (safeModule != null) {
return safeModule
// if `internalModule` has not been set, and no one is creating it. Then this thread
// should call
// create
} else if (!isCreating) {
shouldCreate = true
isCreating = true
} else {
// Wait for `internalModule` to be created by another thread
}
}
if (shouldCreate) {
module = create()
// Once module is built (and initialized if markInitializable has been called), modify
// `internalModule`
// And signal any waiting threads that it is acceptable to read the field now
synchronized(this) {
isCreating = false
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") (this as Object).notifyAll()
}
return module
} else {
synchronized(this) {
// Block waiting for another thread to build `internalModule` instance
// Since isCreating is true until after creation and instantiation (if needed), we wait
// until the module is ready to use.
while (internalModule == null && isCreating) {
try {
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") (this as Object).wait()
} catch (e: InterruptedException) {
continue
}
}
return checkNotNull(internalModule)
}
}
}
private fun create(): NativeModule {
SoftAssertions.assertCondition(internalModule == null, "Creating an already created module.")
ReactMarker.logMarker(ReactMarkerConstants.CREATE_MODULE_START, name, instanceKey)
SystraceMessage.beginSection(TRACE_TAG_REACT, "ModuleHolder.createModule")
.arg("name", name)
.flush()
PrinterHolder.printer.logMessage(
ReactDebugOverlayTags.NATIVE_MODULE,
"NativeModule init: %s",
name,
)
val module: NativeModule
try {
module = checkNotNull(provider).get()
provider = null
var shouldInitializeNow = false
synchronized(this) {
internalModule = module
if (initializable && !isInitializing) {
shouldInitializeNow = true
}
}
if (shouldInitializeNow) {
doInitialize(module)
}
} catch (e: Throwable) {
/**
* When NativeModules are created from JavaScript, any exception that occurs in the creation
* process will have its stack trace swallowed before we display a RedBox to the user. Really,
* we should have our HostObjects on Android understand JniExceptions and log the stack trace
* to logcat. For now, logging to Logcat directly when creation fails is sufficient.
*
* @todo(T53311351)
*/
FLog.e(ReactConstants.TAG, e, "Failed to create NativeModule '%s'", name)
throw e
} finally {
ReactMarker.logMarker(ReactMarkerConstants.CREATE_MODULE_END, name, instanceKey)
SystraceMessage.endSection(TRACE_TAG_REACT).flush()
}
return module
}
private fun doInitialize(module: NativeModule?) {
SystraceMessage.beginSection(TRACE_TAG_REACT, "ModuleHolder.initialize")
.arg("name", name)
.flush()
ReactMarker.logMarker(ReactMarkerConstants.INITIALIZE_MODULE_START, name, instanceKey)
try {
var shouldInitialize = false
// Check to see if another thread is initializing the object, if not claim the responsibility
synchronized(this) {
if (initializable && !isInitializing) {
shouldInitialize = true
isInitializing = true
}
}
if (shouldInitialize) {
module?.initialize()
// Once finished, set flags accordingly, but we don't expect anyone to wait for this to
// finish, so no need to notify other threads.
synchronized(this) { isInitializing = false }
}
} finally {
ReactMarker.logMarker(ReactMarkerConstants.INITIALIZE_MODULE_END, name, instanceKey)
SystraceMessage.endSection(TRACE_TAG_REACT).flush()
}
}
private companion object {
private val instanceKeyCounter = AtomicInteger(1)
}
}

View File

@@ -0,0 +1,60 @@
/*
* 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.bridge
import com.facebook.common.logging.FLog
import com.facebook.react.module.annotations.ReactModule
import javax.inject.Provider
/**
* A specification for a native module. This exists so that we don't have to pay the cost for
* creation until/if the module is used.
*/
public class ModuleSpec
private constructor(
@get:JvmName("provider") public val provider: Provider<out NativeModule>,
@get:JvmName("moduleName") public val name: String? = null,
) {
public fun getProvider(): Provider<out NativeModule> = provider
public fun getName(): String? = name
public companion object {
private const val TAG: String = "ModuleSpec"
@JvmStatic
public fun viewManagerSpec(provider: Provider<out NativeModule>): ModuleSpec =
ModuleSpec(provider)
@JvmStatic
public fun nativeModuleSpec(
type: Class<out NativeModule>,
provider: Provider<out NativeModule>,
): ModuleSpec {
val annotation: ReactModule? = type.getAnnotation(ReactModule::class.java)
return if (annotation == null) {
FLog.w(
TAG,
"Could not find @ReactModule annotation on ${type.name}. " +
"Creating the module eagerly to get the name. Consider adding the annotation.",
)
val nativeModule: NativeModule = provider.get()
ModuleSpec(provider, nativeModule.name)
} else {
ModuleSpec(provider, annotation.name)
}
}
@JvmStatic
public fun nativeModuleSpec(
className: String,
provider: Provider<out NativeModule>,
): ModuleSpec = ModuleSpec(provider, className)
}
}

View File

@@ -0,0 +1,25 @@
/*
* 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.bridge
import com.facebook.jni.HybridClassBase
import com.facebook.proguard.annotations.DoNotStrip
/** Base class for an array whose members are stored in native code (C++). */
@DoNotStrip
public abstract class NativeArray protected constructor() :
HybridClassBase(), NativeArrayInterface {
external override fun toString(): String
private companion object {
init {
ReactNativeJniCommonSoLoader.staticInit()
}
}
}

View File

@@ -0,0 +1,12 @@
/*
* 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.bridge
public interface NativeArrayInterface {
override fun toString(): String
}

View File

@@ -0,0 +1,23 @@
/*
* 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.bridge
import com.facebook.jni.HybridClassBase
import com.facebook.proguard.annotations.DoNotStrip
/** Base class for a Map whose keys and values are stored in native code (C++). */
@DoNotStrip
public abstract class NativeMap : HybridClassBase() {
external override fun toString(): String
private companion object {
init {
ReactNativeJniCommonSoLoader.staticInit()
}
}
}

View File

@@ -0,0 +1,68 @@
/*
* 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.bridge;
import com.facebook.infer.annotation.Nullsafe;
import com.facebook.proguard.annotations.DoNotStrip;
import com.facebook.react.common.annotations.StableReactNativeAPI;
import javax.annotation.Nonnull;
/*
* IMPORTANT: Do not migrate this interface to Kotlin as you'll create a breaking change for React Native
* libraries written in Kotlin.
*/
/**
* A native module whose API can be provided to JS catalyst instances. {@link NativeModule}s whose
* implementation is written in Java should extend {@link BaseJavaModule} or {@link
* ReactContextBaseJavaModule}. {@link NativeModule}s whose implementation is written in C++ must
* not provide any Java code (so they can be reused on other platforms), and instead should register
* themselves using {@link CxxModuleWrapper}.
*/
@Nullsafe(Nullsafe.Mode.LOCAL)
@StableReactNativeAPI
@DoNotStrip
public interface NativeModule {
/**
* @return the name of this module. This will be the name used to {@code require()} this module
* from javascript.
*/
// IMPORTANT: Do not migrate this interface to Kotlin as you'll create a breaking change
// for React Native libraries written in Kotlin
@Nonnull
String getName();
/** This method is called after {@link ReactApplicationContext} has been created. */
void initialize();
/** Allow NativeModule to clean up. Called before React Native instance is destroyed. */
void invalidate();
/**
* Return true if you intend to override some other native module that was registered e.g. as part
* of a different package (such as the core one). Trying to override without returning true from
* this method is considered an error and will throw an exception during initialization. By
* default all modules return false.
*
* @deprecated The method canOverrideExistingModule is not used in the New Architecture and will
* be removed in a future release.
*/
@Deprecated
default boolean canOverrideExistingModule() {
return false;
}
/**
* Allow NativeModule to clean up. Called before {CatalystInstance#onHostDestroy}
*
* @deprecated use {@link #invalidate()} instead.
*/
@Deprecated(since = "Use invalidate method instead", forRemoval = true)
default void onCatalystInstanceDestroy() {}
}

View File

@@ -0,0 +1,140 @@
/*
* 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.
*/
@file:Suppress("DEPRECATION")
package com.facebook.react.bridge
import com.facebook.react.bridge.ReactMarker.logMarker
import com.facebook.react.common.annotations.internal.LegacyArchitecture
import com.facebook.react.common.annotations.internal.LegacyArchitectureLogLevel
import com.facebook.react.common.annotations.internal.LegacyArchitectureLogger
import com.facebook.react.common.annotations.internal.LegacyArchitectureLogger.assertLegacyArchitecture
import com.facebook.react.module.annotations.ReactModule
import com.facebook.systrace.Systrace
import com.facebook.systrace.Systrace.beginSection
import com.facebook.systrace.Systrace.endSection
/** A set of Java APIs to expose to a particular JavaScript instance. */
@LegacyArchitecture(logLevel = LegacyArchitectureLogLevel.ERROR)
@Deprecated(
message = "This class is part of Legacy Architecture and will be removed in a future release",
level = DeprecationLevel.WARNING,
)
public class NativeModuleRegistry(
private val reactApplicationContext: ReactApplicationContext,
private val modules: MutableMap<String, ModuleHolder>,
) {
/** Private getters for combining NativeModuleRegistry's */
private val moduleMap: Map<String, ModuleHolder>
get() = modules
@JvmName("getJavaModules") // This is needed because this method is accessed by JNI
internal fun getJavaModules(jsInstance: JSInstance): List<JavaModuleWrapper> = buildList {
for ((_, value) in modules) {
add(JavaModuleWrapper(jsInstance, value))
}
}
/** Adds any new modules to the current module registry */
@JvmName(
"registerModules"
) // This is needed till there are Java Consumer of this API inside React
// Native
internal fun registerModules(newRegister: NativeModuleRegistry) {
val ownContext = reactApplicationContext
val otherContext = newRegister.reactApplicationContext
check(ownContext == otherContext) {
"Extending native modules with non-matching application contexts."
}
val newModules = newRegister.moduleMap
for ((key, value) in newModules) {
if (!modules.containsKey(key)) {
modules[key] = value
}
}
}
@JvmName(
"notifyJSInstanceDestroy"
) // This is needed till there are Java Consumer of this API inside
// React Native
internal fun notifyJSInstanceDestroy() {
reactApplicationContext.assertOnNativeModulesQueueThread()
beginSection(Systrace.TRACE_TAG_REACT, "NativeModuleRegistry_notifyJSInstanceDestroy")
try {
for (module in modules.values) {
module.destroy()
}
} finally {
endSection(Systrace.TRACE_TAG_REACT)
}
}
@JvmName("notifyJSInstanceInitialized") // This is needed till there are Java Consumer of this API
// inside React Native
internal fun notifyJSInstanceInitialized() {
reactApplicationContext.assertOnNativeModulesQueueThread(
"From version React Native v0.44, " +
"native modules are explicitly not initialized on the UI thread."
)
logMarker(ReactMarkerConstants.NATIVE_MODULE_INITIALIZE_START)
beginSection(Systrace.TRACE_TAG_REACT, "NativeModuleRegistry_notifyJSInstanceInitialized")
try {
for (module in modules.values) {
module.markInitializable()
}
} finally {
endSection(Systrace.TRACE_TAG_REACT)
logMarker(ReactMarkerConstants.NATIVE_MODULE_INITIALIZE_END)
}
}
public fun <T : NativeModule> hasModule(moduleInterface: Class<T>): Boolean {
val annotation = moduleInterface.getAnnotation(ReactModule::class.java)
requireNotNull(annotation) {
"Could not find @ReactModule annotation in class " + moduleInterface.name
}
val name = annotation.name
return modules.containsKey(name)
}
public fun <T : NativeModule> getModule(moduleInterface: Class<T>): T {
val annotation = moduleInterface.getAnnotation(ReactModule::class.java)
requireNotNull(annotation) {
"Could not find @ReactModule annotation in class " + moduleInterface.name
}
@Suppress("UNCHECKED_CAST")
return checkNotNull(modules[annotation.name]) {
"$annotation.name could not be found. Is it defined in ${moduleInterface.name}"
}
.module as T
}
public fun hasModule(name: String): Boolean = modules.containsKey(name)
public fun getModule(name: String): NativeModule =
checkNotNull(modules[name]) { "Could not find module with name $name" }.module
public val allModules: List<NativeModule>
get() = buildList {
for (module in modules.values) {
add(module.module)
}
}
private companion object {
init {
LegacyArchitectureLogger.assertLegacyArchitecture(
"NativeModuleRegistry",
logLevel = LegacyArchitectureLogLevel.ERROR,
)
}
}
}

View File

@@ -0,0 +1,14 @@
/*
* 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.bridge
import com.facebook.proguard.annotations.DoNotStrip
/** Exception thrown by [ReadableNativeMap] when a key that does not exist is requested. */
@DoNotStrip
public class NoSuchKeyException @DoNotStrip constructor(msg: String) : RuntimeException(msg)

View File

@@ -0,0 +1,19 @@
/*
* 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.bridge
import com.facebook.proguard.annotations.DoNotStrip
/**
* Exception thrown when a caller attempts to modify or use a [WritableNativeArray] or
* [WritableNativeMap] after it has already been added to a parent array or map. This is unsafe
* since we reuse the native memory so the underlying array/map is no longer valid.
*/
@DoNotStrip
internal class ObjectAlreadyConsumedException @DoNotStrip constructor(detailMessage: String) :
RuntimeException(detailMessage)

View File

@@ -0,0 +1,13 @@
/*
* 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.bridge
public interface PerformanceCounter {
public fun profileNextBatch()
public val performanceCounters: Map<String, Long>?
}

View File

@@ -0,0 +1,115 @@
/*
* 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.bridge
/*
* Interface that represents a JavaScript Promise which can be passed to the native module as a
* method parameter.
*
* Methods annotated with [ReactMethod] that use a [Promise] as the last parameter
* will be marked as "promise" and will return a promise when invoked from JavaScript.
*/
public interface Promise {
/**
* Successfully resolve the Promise with an optional value.
*
* @param value Object
*/
public fun resolve(value: Any?)
/**
* Report an error without an exception using a custom code and error message.
*
* @param code String
* @param message String
*/
public fun reject(code: String?, message: String?)
/**
* Report an exception with a custom code.
*
* @param code String
* @param throwable Throwable
*/
public fun reject(code: String?, throwable: Throwable?)
/**
* Report an exception with a custom code and error message.
*
* @param code String
* @param message String
* @param throwable Throwable
*/
public fun reject(code: String?, message: String?, throwable: Throwable?)
/**
* Report an exception, with default error code. Useful in catch-all scenarios where it's unclear
* why the error occurred.
*
* @param throwable Throwable
*/
public fun reject(throwable: Throwable)
/* ---------------------------
* With userInfo WritableMap
* --------------------------- */
/**
* Report an exception, with default error code, with userInfo. Useful in catch-all scenarios
* where it's unclear why the error occurred.
*
* @param throwable Throwable
* @param userInfo WritableMap
*/
public fun reject(throwable: Throwable, userInfo: WritableMap)
/**
* Reject with a code and userInfo WritableMap.
*
* @param code String
* @param userInfo WritableMap
*/
public fun reject(code: String?, userInfo: WritableMap)
/**
* Report an exception with a custom code and userInfo.
*
* @param code String
* @param throwable Throwable
* @param userInfo WritableMap
*/
public fun reject(code: String?, throwable: Throwable?, userInfo: WritableMap)
/**
* Report an error with a custom code, error message and userInfo, an error not caused by an
* exception.
*
* @param code String
* @param message String
* @param userInfo WritableMap
*/
public fun reject(code: String?, message: String?, userInfo: WritableMap)
/**
* Report an exception with a custom code, error message and userInfo.
*
* @param code String
* @param message String
* @param throwable Throwable
* @param userInfo WritableMap
*/
public fun reject(code: String?, message: String?, throwable: Throwable?, userInfo: WritableMap?)
/** Report an error which wasn't caused by an exception. */
@Deprecated(
message =
"""Prefer passing a module-specific error code to JS. Using this method will pass the
error code EUNSPECIFIED""",
replaceWith = ReplaceWith("reject(code, message)"),
)
public fun reject(message: String)
}

Some files were not shown because too many files have changed in this diff Show More