first commit

This commit is contained in:
2026-03-10 16:18:05 +00:00
commit 11f9c069b5
31635 changed files with 3187747 additions and 0 deletions

8
node_modules/react-native/ReactAndroid/README.md generated vendored Normal file
View File

@@ -0,0 +1,8 @@
# Building React Native for Android
See the [Building from Source guide](https://reactnative.dev/contributing/how-to-build-from-source#prerequisites) on the React Native website.
# Running tests
When you submit a pull request, ci will automatically run all tests.
To run tests locally, see [Testing guide](https://reactnative.dev/contributing/how-to-run-and-write-tests) on the React Native website.

File diff suppressed because it is too large Load Diff

753
node_modules/react-native/ReactAndroid/build.gradle.kts generated vendored Normal file
View File

@@ -0,0 +1,753 @@
/*
* 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.
*/
import com.android.build.gradle.internal.tasks.factory.dependsOn
import com.facebook.react.internal.PrivateReactExtension
import com.facebook.react.tasks.internal.*
import com.facebook.react.tasks.internal.utils.*
import de.undercouch.gradle.tasks.download.Download
import java.nio.file.Paths
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("maven-publish")
id("com.facebook.react")
alias(libs.plugins.android.library)
alias(libs.plugins.download)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.ktfmt)
}
version = project.findProperty("VERSION_NAME")?.toString()!!
group = "com.facebook.react"
// We download various C++ open-source dependencies into downloads.
// We then copy both the downloaded code and our custom makefiles and headers into third-party-ndk.
// After that we build native code from src/main/jni with module path pointing at third-party-ndk.
val buildDir = project.layout.buildDirectory.get().asFile
val downloadsDir =
if (System.getenv("REACT_NATIVE_DOWNLOADS_DIR") != null) {
File(System.getenv("REACT_NATIVE_DOWNLOADS_DIR"))
} else {
File("$buildDir/downloads")
}
val thirdPartyNdkDir = File("$buildDir/third-party-ndk")
val reactNativeRootDir = projectDir.parent
val hermesV1Enabled =
rootProject.extensions.getByType(PrivateReactExtension::class.java).hermesV1Enabled.get()
// We put the publishing version from gradle.properties inside ext. so other
// subprojects can access it as well.
extra["publishing_version"] = project.findProperty("VERSION_NAME")?.toString()!!
// This is the version of CMake we're requesting to the Android SDK to use.
// If missing it will be downloaded automatically. Only CMake versions shipped with the
// Android SDK are supported (you can find them listed in the SDK Manager of Android Studio).
val cmakeVersion = System.getenv("CMAKE_VERSION") ?: "3.30.5"
extra["cmake_version"] = cmakeVersion
// You need to have following folders in this directory:
// - boost_1_83_0
// - double-conversion-1.1.6
// - folly-deprecate-dynamic-initializer
// - glog-0.3.5
val dependenciesPath = System.getenv("REACT_NATIVE_DEPENDENCIES")
// The Boost library is a very large download (>100MB).
// If Boost is already present on your system, define the REACT_NATIVE_BOOST_PATH env variable
// and the build will use that.
val boostPathOverride = dependenciesPath ?: System.getenv("REACT_NATIVE_BOOST_PATH")
val prefabHeadersDir = project.file("$buildDir/prefab-headers")
// Native versions which are defined inside the version catalog (libs.versions.toml)
val BOOST_VERSION = libs.versions.boost.get()
val DOUBLE_CONVERSION_VERSION = libs.versions.doubleconversion.get()
val FAST_FLOAT_VERSION = libs.versions.fastFloat.get()
val FMT_VERSION = libs.versions.fmt.get()
val FOLLY_VERSION = libs.versions.folly.get()
val GLOG_VERSION = libs.versions.glog.get()
val preparePrefab by
tasks.registering(PreparePrefabHeadersTask::class) {
dependsOn(
prepareBoost,
prepareDoubleConversion,
prepareFastFloat,
prepareFmt,
prepareFolly,
prepareGlog,
)
dependsOn("generateCodegenArtifactsFromSchema")
// To export to a ReactNativePrefabProcessingEntities.kt once all
// libraries have been moved. We keep it here for now as it make easier to
// migrate one library at a time.
input.set(
listOf(
PrefabPreprocessingEntry("jsi", Pair("../ReactCommon/jsi/", "")),
PrefabPreprocessingEntry(
"reactnative",
listOf(
// hermes_executor
// This prefab targets is used by Expo & Reanimated
Pair("../ReactCommon/hermes/inspector-modern/", "hermes/inspector-modern/"),
// fabricjni
Pair("src/main/jni/react/fabric", "react/fabric/"),
// glog
Pair(File(buildDir, "third-party-ndk/glog/exported/").absolutePath, ""),
// jsiinpsector
Pair("../ReactCommon/jsinspector-modern/", "jsinspector-modern/"),
// mapbufferjni
Pair("src/main/jni/react/mapbuffer", ""),
// turbomodulejsijni
Pair("src/main/jni/react/turbomodule", ""),
// react_codegen_rncore
Pair(File(buildDir, "generated/source/codegen/jni/").absolutePath, ""),
// reactnativejni
Pair("src/main/jni/react/jni", "react/jni/"),
Pair("../ReactCommon/cxxreact/", "cxxreact/"),
// react_featureflags
Pair("../ReactCommon/react/featureflags/", "react/featureflags/"),
// react_devtoolsruntimesettings
Pair(
"../ReactCommon/react/devtoolsruntimesettings/",
"react/devtoolsruntimesettings/",
),
// react_renderer_animations
Pair(
"../ReactCommon/react/renderer/animations/",
"react/renderer/animations/",
),
// react_renderer_bridging
Pair("../ReactCommon/react/renderer/bridging/", "react/renderer/bridging/"),
// react_renderer_componentregistry
Pair(
"../ReactCommon/react/renderer/componentregistry/",
"react/renderer/componentregistry/",
),
// react_renderer_consistency
Pair(
"../ReactCommon/react/renderer/consistency/",
"react/renderer/consistency/",
),
// react_renderer_core
Pair("../ReactCommon/react/renderer/core/", "react/renderer/core/"),
// react_renderer_css
Pair("../ReactCommon/react/renderer/css/", "react/renderer/css/"),
// react_debug
Pair("../ReactCommon/react/debug/", "react/debug/"),
// react_renderer_debug
Pair("../ReactCommon/react/renderer/debug/", "react/renderer/debug/"),
// react_renderer_graphics
Pair("../ReactCommon/react/renderer/graphics/", "react/renderer/graphics/"),
Pair("../ReactCommon/react/renderer/graphics/platform/android/", ""),
// react_renderer_imagemanager
Pair(
"../ReactCommon/react/renderer/imagemanager/",
"react/renderer/imagemanager/",
),
Pair("../ReactCommon/react/renderer/imagemanager/platform/cxx/", ""),
// react_renderer_mounting
Pair("../ReactCommon/react/renderer/mounting/", "react/renderer/mounting/"),
// react_renderer_scheduler
Pair("../ReactCommon/react/renderer/scheduler/", "react/renderer/scheduler/"),
// react_renderer_uimanager
Pair("../ReactCommon/react/renderer/uimanager/", "react/renderer/uimanager/"),
// react_utils
Pair("../ReactCommon/react/utils/", "react/utils/"),
// rrc_image
Pair(
"../ReactCommon/react/renderer/components/image/",
"react/renderer/components/image/",
),
// rrc_view
Pair(
"../ReactCommon/react/renderer/components/view/",
"react/renderer/components/view/",
),
Pair("../ReactCommon/react/renderer/components/view/platform/android/", ""),
// rrc_root
Pair(
"../ReactCommon/react/renderer/components/root/",
"react/renderer/components/root/",
),
// runtimeexecutor
Pair("../ReactCommon/runtimeexecutor/", ""),
// react_renderer_textlayoutmanager
Pair(
"../ReactCommon/react/renderer/textlayoutmanager/",
"react/renderer/textlayoutmanager/",
),
Pair("../ReactCommon/react/renderer/textlayoutmanager/platform/android/", ""),
// rrc_text
Pair(
"../ReactCommon/react/renderer/components/text/",
"react/renderer/components/text/",
),
Pair(
"../ReactCommon/react/renderer/attributedstring",
"react/renderer/attributedstring",
),
// rrc_textinput
Pair(
"../ReactCommon/react/renderer/components/textinput/",
"react/renderer/components/textinput/",
),
Pair(
"../ReactCommon/react/renderer/components/textinput/platform/android/",
"",
),
// react_newarchdefaults
Pair("src/main/jni/react/newarchdefaults", ""),
// react_nativemodule_core
Pair(File(buildDir, "third-party-ndk/boost/boost_1_83_0/").absolutePath, ""),
Pair(File(buildDir, "third-party-ndk/double-conversion/").absolutePath, ""),
Pair(File(buildDir, "third-party-ndk/fast_float/include/").absolutePath, ""),
Pair(File(buildDir, "third-party-ndk/fmt/include/").absolutePath, ""),
Pair(File(buildDir, "third-party-ndk/folly/").absolutePath, ""),
Pair(File(buildDir, "third-party-ndk/glog/exported/").absolutePath, ""),
Pair("../ReactCommon/callinvoker/", ""),
Pair("../ReactCommon/cxxreact/", "cxxreact/"),
Pair("../ReactCommon/react/bridging/", "react/bridging/"),
Pair("../ReactCommon/react/nativemodule/core/", ""),
Pair("../ReactCommon/react/nativemodule/core/platform/android/", ""),
Pair(
"../ReactCommon/react/renderer/componentregistry/",
"react/renderer/componentregistry/",
),
Pair(
"../ReactCommon/react/renderer/components/root/",
"react/renderer/components/root/",
),
Pair("../ReactCommon/react/renderer/core/", "react/renderer/core/"),
Pair("../ReactCommon/react/renderer/debug/", "react/renderer/debug/"),
Pair(
"../ReactCommon/react/renderer/leakchecker/",
"react/renderer/leakchecker/",
),
Pair("../ReactCommon/react/renderer/mapbuffer/", "react/renderer/mapbuffer/"),
Pair("../ReactCommon/react/renderer/mounting/", "react/renderer/mounting/"),
Pair(
"../ReactCommon/react/renderer/runtimescheduler/",
"react/renderer/runtimescheduler/",
),
Pair("../ReactCommon/react/renderer/scheduler/", "react/renderer/scheduler/"),
Pair("../ReactCommon/react/renderer/telemetry/", "react/renderer/telemetry/"),
Pair("../ReactCommon/react/renderer/uimanager/", "react/renderer/uimanager/"),
Pair("../ReactCommon/react/debug/", "react/debug/"),
Pair("../ReactCommon/react/utils/", "react/utils/"),
Pair("src/main/jni/react/jni", "react/jni/"),
// react_cxxreactpackage
Pair("src/main/jni/react/runtime/cxxreactpackage", ""),
// react_performance_timeline
Pair(
"../ReactCommon/react/performance/timeline/",
"react/performance/timeline/",
),
// react_performance_cdpmetrics
Pair(
"../ReactCommon/react/performance/cdpmetrics/",
"react/performance/cdpmetrics/",
),
// react_renderer_observers_events
Pair(
"../ReactCommon/react/renderer/observers/events/",
"react/renderer/observers/events/",
),
// react_timing
Pair("../ReactCommon/react/timing/", "react/timing/"),
// yoga
Pair("../ReactCommon/yoga/", ""),
Pair("src/main/jni/first-party/yogajni/jni", ""),
),
),
PrefabPreprocessingEntry(
"hermestooling",
// hermes_executor
Pair("../ReactCommon/hermes/inspector-modern/", "hermes/inspector-modern/"),
),
)
)
outputDir.set(prefabHeadersDir)
}
val createNativeDepsDirectories by
tasks.registering {
downloadsDir.mkdirs()
thirdPartyNdkDir.mkdirs()
}
val downloadBoostDest = File(downloadsDir, "boost_${BOOST_VERSION}.tar.gz")
val downloadBoost by
tasks.registering(Download::class) {
dependsOn(createNativeDepsDirectories)
src(
"https://archives.boost.io/release/${BOOST_VERSION.replace("_", ".")}/source/boost_${BOOST_VERSION}.tar.gz"
)
onlyIfModified(true)
overwrite(false)
retries(5)
quiet(true)
dest(downloadBoostDest)
}
val prepareBoost by
tasks.registering(PrepareBoostTask::class) {
dependsOn(if (boostPathOverride != null) emptyList() else listOf(downloadBoost))
boostPath.setFrom(if (boostPathOverride != null) boostPath else tarTree(downloadBoostDest))
boostThirdPartyJniPath.set(project.file("src/main/jni/third-party/boost"))
boostVersion.set(BOOST_VERSION)
outputDir.set(File(thirdPartyNdkDir, "boost"))
}
val downloadDoubleConversionDest =
File(downloadsDir, "double-conversion-${DOUBLE_CONVERSION_VERSION}.tar.gz")
val downloadDoubleConversion by
tasks.registering(Download::class) {
dependsOn(createNativeDepsDirectories)
src(
"https://github.com/google/double-conversion/archive/v${DOUBLE_CONVERSION_VERSION}.tar.gz"
)
onlyIfModified(true)
overwrite(false)
retries(5)
quiet(true)
dest(downloadDoubleConversionDest)
}
val prepareDoubleConversion by
tasks.registering(Copy::class) {
dependsOn(if (dependenciesPath != null) emptyList() else listOf(downloadDoubleConversion))
from(dependenciesPath ?: tarTree(downloadDoubleConversionDest))
from("src/main/jni/third-party/double-conversion/")
include("double-conversion-${DOUBLE_CONVERSION_VERSION}/src/**/*", "CMakeLists.txt")
filesMatching("*/src/**/*") { path = "double-conversion/${name}" }
includeEmptyDirs = false
into("$thirdPartyNdkDir/double-conversion")
}
val downloadFollyDest = File(downloadsDir, "folly-${FOLLY_VERSION}.tar.gz")
val downloadFolly by
tasks.registering(Download::class) {
src("https://github.com/facebook/folly/archive/v${FOLLY_VERSION}.tar.gz")
onlyIfModified(true)
overwrite(false)
retries(5)
quiet(true)
dest(downloadFollyDest)
}
val prepareFolly by
tasks.registering(Copy::class) {
dependsOn(if (dependenciesPath != null) emptyList() else listOf(downloadFolly))
from(dependenciesPath ?: tarTree(downloadFollyDest))
from("src/main/jni/third-party/folly/")
include("folly-${FOLLY_VERSION}/folly/**/*", "CMakeLists.txt")
eachFile { path = path.substringAfter("/") }
includeEmptyDirs = false
into("$thirdPartyNdkDir/folly")
}
val downloadFastFloatDest = File(downloadsDir, "fast_float-${FAST_FLOAT_VERSION}.tar.gz")
val downloadFastFloat by
tasks.registering(Download::class) {
dependsOn(createNativeDepsDirectories)
src("https://github.com/fastfloat/fast_float/archive/v${FAST_FLOAT_VERSION}.tar.gz")
onlyIfModified(true)
overwrite(false)
retries(5)
quiet(true)
dest(downloadFastFloatDest)
}
val prepareFastFloat by
tasks.registering(Copy::class) {
dependsOn(if (dependenciesPath != null) emptyList() else listOf(downloadFastFloat))
from(dependenciesPath ?: tarTree(downloadFastFloatDest))
from("src/main/jni/third-party/fast_float/")
include("fast_float-${FAST_FLOAT_VERSION}/include/**/*", "CMakeLists.txt")
eachFile { path = path.substringAfter("/") }
includeEmptyDirs = false
into("$thirdPartyNdkDir/fast_float")
}
val downloadFmtDest = File(downloadsDir, "fmt-${FMT_VERSION}.tar.gz")
val downloadFmt by
tasks.registering(Download::class) {
dependsOn(createNativeDepsDirectories)
src("https://github.com/fmtlib/fmt/archive/${FMT_VERSION}.tar.gz")
onlyIfModified(true)
overwrite(false)
retries(5)
quiet(true)
dest(downloadFmtDest)
}
val prepareFmt by
tasks.registering(Copy::class) {
dependsOn(if (dependenciesPath != null) emptyList() else listOf(downloadFmt))
from(dependenciesPath ?: tarTree(downloadFmtDest))
from("src/main/jni/third-party/fmt/")
include("fmt-${FMT_VERSION}/src/**/*", "fmt-${FMT_VERSION}/include/**/*", "CMakeLists.txt")
eachFile { path = path.substringAfter("/") }
includeEmptyDirs = false
into("$thirdPartyNdkDir/fmt")
}
val downloadGlogDest = File(downloadsDir, "glog-${GLOG_VERSION}.tar.gz")
val downloadGlog by
tasks.registering(Download::class) {
dependsOn(createNativeDepsDirectories)
src("https://github.com/google/glog/archive/v${GLOG_VERSION}.tar.gz")
onlyIfModified(true)
overwrite(false)
retries(5)
quiet(true)
dest(downloadGlogDest)
}
val prepareGlog by
tasks.registering(PrepareGlogTask::class) {
dependsOn(if (dependenciesPath != null) emptyList() else listOf(downloadGlog))
glogPath.setFrom(dependenciesPath ?: tarTree(downloadGlogDest))
glogThirdPartyJniPath.set(project.file("src/main/jni/third-party/glog/"))
glogVersion.set(GLOG_VERSION)
outputDir.set(File(thirdPartyNdkDir, "glog"))
}
// Tasks used by Fantom to download the Native 3p dependencies used.
val prepareNative3pDependencies by
tasks.registering {
dependsOn(
prepareBoost,
prepareDoubleConversion,
prepareFastFloat,
prepareFmt,
prepareFolly,
prepareGlog,
)
}
val prepareKotlinBuildScriptModel by
tasks.registering {
// This task is run when Gradle Sync is running.
// We create it here so we can let it depend on preBuild inside the android{}
}
// As ReactAndroid builds from source, the codegen needs to be built before it can be invoked.
// This is not the case for users of React Native, as we ship a compiled version of the codegen.
val buildCodegenCLI by
tasks.registering(BuildCodegenCLITask::class) {
codegenDir.set(file("$rootDir/node_modules/@react-native/codegen"))
bashWindowsHome.set(project.findProperty("react.internal.windowsBashPath").toString())
logFile.set(file("$buildDir/codegen.log"))
inputFiles.set(fileTree(codegenDir) { include("src/**/*.js") })
outputFiles.set(
fileTree(codegenDir) {
include("lib/**/*.js")
include("lib/**/*.js.flow")
}
)
rootProjectName.set(rootProject.name)
}
/**
* Finds the path of the installed npm package with the given name using Node's module resolution
* algorithm, which searches "node_modules" directories up to the file system root. This handles
* various cases, including:
* - Working in the open-source RN repo: Gradle: /path/to/react-native/ReactAndroid Node module:
* /path/to/react-native/node_modules/<package>
* - Installing RN as a dependency of an app and searching for hoisted dependencies: Gradle:
* /path/to/app/node_modules/react-native/ReactAndroid Node module:
* /path/to/app/node_modules/<package>
* - Working in a larger repo (e.g., Facebook) that contains RN: Gradle:
* /path/to/repo/path/to/react-native/ReactAndroid Node module:
* /path/to/repo/node_modules/<package>
*
* The search begins at the given base directory (a File object). The returned path is a string.
*/
fun findNodeModulePath(baseDir: File, packageName: String): String? {
var basePath: java.nio.file.Path? = baseDir.toPath().normalize()
// Node's module resolution algorithm searches up to the root directory,
// after which the base path will be null
while (basePath != null) {
val candidatePath = Paths.get(basePath.toString(), "node_modules", packageName)
if (candidatePath.toFile().exists()) {
return candidatePath.toString()
}
basePath = basePath.parent
}
return null
}
fun reactNativeDevServerPort(): String {
val value = project.properties["reactNativeDevServerPort"]
return value?.toString() ?: "8081"
}
fun reactNativeArchitectures(): List<String> {
val value = project.properties["reactNativeArchitectures"]
return value?.toString()?.split(",") ?: listOf("armeabi-v7a", "x86", "x86_64", "arm64-v8a")
}
fun enableWarningsAsErrors(): Boolean {
val value = project.properties["enableWarningsAsErrors"]
return value?.toString()?.toBoolean() ?: false
}
repositories {
// Normally RNGP will set repositories for all modules,
// but when consumed from source, we need to re-declare
// those repositories as there is no app module there.
mavenCentral()
google()
}
android {
compileSdk = libs.versions.compileSdk.get().toInt()
buildToolsVersion = libs.versions.buildTools.get()
namespace = "com.facebook.react"
// Used to override the NDK path/version on internal CI or by allowing
// users to customize the NDK path/version from their root project (e.g. for Apple Silicon
// support)
if (rootProject.hasProperty("ndkPath") && rootProject.properties["ndkPath"] != null) {
ndkPath = rootProject.properties["ndkPath"].toString()
}
if (rootProject.hasProperty("ndkVersion") && rootProject.properties["ndkVersion"] != null) {
ndkVersion = rootProject.properties["ndkVersion"].toString()
} else {
ndkVersion = libs.versions.ndkVersion.get()
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlin {
compilerOptions {
// Using '-Xjvm-default=all' to generate default java methods for interfaces
freeCompilerArgs = listOf("-Xjvm-default=all")
// Using -PenableWarningsAsErrors=true prop to enable allWarningsAsErrors
allWarningsAsErrors = enableWarningsAsErrors()
}
}
defaultConfig {
minSdk = libs.versions.minSdk.get().toInt()
consumerProguardFiles("proguard-rules.pro")
buildConfigField("boolean", "IS_INTERNAL_BUILD", "false")
buildConfigField("int", "EXOPACKAGE_FLAGS", "0")
buildConfigField("boolean", "UNSTABLE_ENABLE_FUSEBOX_RELEASE", "false")
buildConfigField("boolean", "ENABLE_PERFETTO", "false")
buildConfigField("boolean", "UNSTABLE_ENABLE_MINIFY_LEGACY_ARCHITECTURE", "false")
resValue("integer", "react_native_dev_server_port", reactNativeDevServerPort())
resValue("string", "react_native_dev_server_ip", "localhost")
testApplicationId = "com.facebook.react.tests.gradle"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
externalNativeBuild {
cmake {
arguments(
"-DREACT_COMMON_DIR=${reactNativeRootDir}/ReactCommon",
"-DREACT_ANDROID_DIR=$projectDir",
"-DREACT_BUILD_DIR=$buildDir",
"-DANDROID_STL=c++_shared",
"-DANDROID_TOOLCHAIN=clang",
"-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON",
"-DCMAKE_POLICY_DEFAULT_CMP0069=NEW",
)
if (hermesV1Enabled) {
arguments("-DHERMES_V1_ENABLED=1")
}
targets(
"reactnative",
"jsi",
"hermestooling",
)
}
}
ndk { abiFilters.addAll(reactNativeArchitectures()) }
}
externalNativeBuild {
cmake {
version = cmakeVersion
path("src/main/jni/CMakeLists.txt")
}
}
tasks
.getByName("preBuild")
.dependsOn(
buildCodegenCLI,
"generateCodegenArtifactsFromSchema",
prepareNative3pDependencies,
preparePrefab,
)
tasks.getByName("generateCodegenSchemaFromJavaScript").dependsOn(buildCodegenCLI)
prepareKotlinBuildScriptModel.dependsOn("preBuild")
prepareKotlinBuildScriptModel.dependsOn(
":packages:react-native:ReactAndroid:hermes-engine:preBuild"
)
sourceSets.getByName("main") {
res.setSrcDirs(
listOf(
"src/main/res/devsupport",
"src/main/res/shell",
"src/main/res/views/alert",
"src/main/res/views/modal",
"src/main/res/views/uimanager",
"src/main/res/views/view",
)
)
java.exclude("com/facebook/react/processing")
java.exclude("com/facebook/react/module/processing")
}
lint {
abortOnError = false
targetSdk = libs.versions.targetSdk.get().toInt()
}
packaging {
resources.excludes.add("META-INF/NOTICE")
resources.excludes.add("META-INF/LICENSE")
// We intentionally don't want to bundle any JS Runtime inside the Android AAR
// we produce. The reason behind this is that we want to allow users to pick the
// JS engine by specifying a dependency on either `hermes-engine` or other engines
// that will include the necessary .so files to load.
jniLibs.excludes.add("**/libhermesvm.so")
}
buildFeatures {
prefab = true
prefabPublishing = true
buildConfig = true
resValues = true
}
prefab {
create("jsi") { headers = File(prefabHeadersDir, "jsi").absolutePath }
create("reactnative") { headers = File(prefabHeadersDir, "reactnative").absolutePath }
create("hermestooling") { headers = File(prefabHeadersDir, "hermestooling").absolutePath }
}
publishing {
multipleVariants {
withSourcesJar()
allVariants()
}
}
testOptions {
unitTests { isIncludeAndroidResources = true }
targetSdk = libs.versions.targetSdk.get().toInt()
}
buildTypes {
create("debugOptimized") {
initWith(getByName("debug"))
externalNativeBuild {
cmake { arguments("-DCMAKE_BUILD_TYPE=Release", "-DREACT_NATIVE_DEBUG_OPTIMIZED=True") }
}
}
}
}
tasks.withType<KotlinCompile>().configureEach {
exclude("com/facebook/annotationprocessors/**")
exclude("com/facebook/react/processing/**")
exclude("com/facebook/react/module/processing/**")
}
dependencies {
api(libs.androidx.appcompat)
api(libs.androidx.appcompat.resources)
api(libs.androidx.autofill)
api(libs.androidx.swiperefreshlayout)
api(libs.androidx.tracing)
api(libs.fbjni)
api(libs.fresco)
api(libs.fresco.imagepipeline.okhttp3)
api(libs.fresco.middleware)
api(libs.fresco.ui.common)
api(libs.infer.annotation)
api(libs.soloader)
api(libs.yoga.proguard.annotations)
api(libs.jsr305)
api(libs.okhttp3.urlconnection)
api(libs.okhttp3)
api(libs.okio)
compileOnly(libs.javax.annotation.api)
api(libs.javax.inject)
// It's up to the consumer to decide if hermes or other engines should be included or not.
// Therefore hermes-engine is a compileOnly dependencies.
compileOnly(project(":packages:react-native:ReactAndroid:hermes-engine"))
testImplementation(libs.junit)
testImplementation(libs.assertj)
testImplementation(libs.mockito)
testImplementation(libs.mockito.kotlin)
testImplementation(libs.robolectric)
testImplementation(libs.thoughtworks)
}
react {
libraryName = "FBReactNativeSpec"
jsRootDir = file("../src")
}
// For build from source, we need to override the privateReact extension.
// This is needed as the build-from-source won't have a com.android.application
// module to apply the plugin to, so it's codegenDir and reactNativeDir won't be evaluated.
if (rootProject.name == "react-native-build-from-source") {
rootProject.extensions.getByType(PrivateReactExtension::class.java).apply {
// We try to guess where codegen lives. Generally is inside
// node_modules/@react-native/codegen. If the file is not existing, we
// fallback to ../react-native-codegen (used for hello-world app).
codegenDir =
if (file("$rootDir/../@react-native/codegen").exists()) {
file("$rootDir/../@react-native/codegen")
} else {
file("$rootDir/../react-native-codegen")
}
reactNativeDir = file("$rootDir")
}
}
kotlin {
jvmToolchain(17)
explicitApi()
}
tasks.withType<Test> { jvmArgs = listOf("-Xshare:off") }
/* Publishing Configuration */
apply(from = "./publish.gradle")
// We need to override the artifact ID as this project is called `ReactAndroid` but
// the maven coordinates are on `react-android`.
// Please note that the original coordinates, `react-native`, have been voided
// as they caused https://github.com/facebook/react-native/issues/35210
publishing {
publications { getByName("release", MavenPublication::class) { artifactId = "react-android" } }
}

View File

@@ -0,0 +1,122 @@
# 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.
# This CMake file takes care of creating everything you need to build and link
# your C++ source code in a React Native Application for Android.
# You just need to call `project(<my_project_name>)` and import this file.
# Specifically this file will:
# - Take care of creating a shared library called as your project
# - Take care of setting the correct compile options
# - Include all the pre-built libraries in your build graph
# - Link your library against those prebuilt libraries so you can access JSI, Fabric, etc.
# - Link your library against any autolinked library.
# - Make sure ccache is used as part of the compilation process, if you have it installed.
cmake_minimum_required(VERSION 3.13)
set(CMAKE_VERBOSE_MAKEFILE on)
include(${CMAKE_CURRENT_LIST_DIR}/folly-flags.cmake)
# We configured the REACT_COMMON_DIR variable as it's commonly used to reference
# shared C++ code in other targets.
set(REACT_COMMON_DIR ${REACT_ANDROID_DIR}/../ReactCommon)
include(${REACT_COMMON_DIR}/cmake-utils/react-native-flags.cmake)
# If you have ccache installed, we're going to honor it.
find_program(CCACHE_FOUND ccache)
if(CCACHE_FOUND)
set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE ccache)
set_property(GLOBAL PROPERTY RULE_LAUNCH_LINK ccache)
endif(CCACHE_FOUND)
# If the user toolchain supports IPO, we enable it for the app build
include(CheckIPOSupported)
check_ipo_supported(RESULT IPO_SUPPORT)
if (IPO_SUPPORT)
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION TRUE)
endif()
set(BUILD_DIR ${PROJECT_BUILD_DIR})
file(TO_CMAKE_PATH "${BUILD_DIR}" BUILD_DIR)
file(TO_CMAKE_PATH "${REACT_ANDROID_DIR}" REACT_ANDROID_DIR)
if (PROJECT_ROOT_DIR)
# This empty `if` is just to silence a CMake warning and make sure the `PROJECT_ROOT_DIR`
# variable is defined if user need to access it.
endif ()
file(GLOB override_cpp_SRC CONFIGURE_DEPENDS *.cpp)
# We check if the user is providing a custom OnLoad.cpp file. If so, we pick that
# for compilation. Otherwise we fallback to using the `default-app-setup/OnLoad.cpp`
# file instead.
if(override_cpp_SRC)
file(GLOB input_SRC CONFIGURE_DEPENDS
*.cpp
${BUILD_DIR}/generated/autolinking/src/main/jni/*.cpp)
else()
file(GLOB input_SRC CONFIGURE_DEPENDS
${REACT_ANDROID_DIR}/cmake-utils/default-app-setup/*.cpp
${BUILD_DIR}/generated/autolinking/src/main/jni/*.cpp)
endif()
add_library(${CMAKE_PROJECT_NAME} SHARED ${input_SRC})
target_include_directories(${CMAKE_PROJECT_NAME}
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}
${PROJECT_BUILD_DIR}/generated/autolinking/src/main/jni)
target_compile_reactnative_options(${CMAKE_PROJECT_NAME} PRIVATE)
# Prefab packages from React Native
find_package(ReactAndroid REQUIRED CONFIG)
add_library(jsi ALIAS ReactAndroid::jsi)
add_library(reactnative ALIAS ReactAndroid::reactnative)
find_package(fbjni REQUIRED CONFIG)
add_library(fbjni ALIAS fbjni::fbjni)
target_link_libraries(${CMAKE_PROJECT_NAME}
fbjni # via 3rd party prefab
jsi # prefab ready
reactnative # prefab ready
)
# We use an interface target to propagate flags to all the generated targets
# such as the folly flags or others.
add_library(common_flags INTERFACE)
target_compile_options(common_flags INTERFACE ${folly_FLAGS})
# If project is on RN CLI v9, then we can use the following lines to link against the autolinked 3rd party libraries.
if(EXISTS ${PROJECT_BUILD_DIR}/generated/autolinking/src/main/jni/Android-autolinking.cmake)
include(${PROJECT_BUILD_DIR}/generated/autolinking/src/main/jni/Android-autolinking.cmake)
target_link_libraries(${CMAKE_PROJECT_NAME} ${AUTOLINKED_LIBRARIES})
foreach(autolinked_library ${AUTOLINKED_LIBRARIES})
target_link_libraries(${autolinked_library} common_flags)
endforeach()
endif()
# If project is running codegen at the app level, we want to link and build the generated library.
if(EXISTS ${PROJECT_BUILD_DIR}/generated/source/codegen/jni/CMakeLists.txt)
add_subdirectory(${PROJECT_BUILD_DIR}/generated/source/codegen/jni/ codegen_app_build)
get_property(APP_CODEGEN_TARGET DIRECTORY ${PROJECT_BUILD_DIR}/generated/source/codegen/jni/ PROPERTY BUILDSYSTEM_TARGETS)
target_link_libraries(${CMAKE_PROJECT_NAME} ${APP_CODEGEN_TARGET})
target_link_libraries(${APP_CODEGEN_TARGET} common_flags)
# We need to pass the generated header and module provider to the OnLoad.cpp file so
# local app modules can properly be linked.
string(REGEX REPLACE "react_codegen_" "" APP_CODEGEN_HEADER "${APP_CODEGEN_TARGET}")
target_compile_options(${CMAKE_PROJECT_NAME}
PRIVATE
-DREACT_NATIVE_APP_CODEGEN_HEADER="${APP_CODEGEN_HEADER}.h"
-DREACT_NATIVE_APP_COMPONENT_DESCRIPTORS_HEADER="react/renderer/components/${APP_CODEGEN_HEADER}/ComponentDescriptors.h"
-DREACT_NATIVE_APP_COMPONENT_REGISTRATION=${APP_CODEGEN_HEADER}_registerComponentDescriptorsFromCodegen
-DREACT_NATIVE_APP_MODULE_PROVIDER=${APP_CODEGEN_HEADER}_ModuleProvider
)
endif()
# We set REACTNATIVE_MERGED_SO so libraries/apps can selectively decide to depend on either libreactnative.so
# or link against a old prefab target (this is needed for React Native 0.76 on).
set(REACTNATIVE_MERGED_SO true)

View File

@@ -0,0 +1,31 @@
# 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.
# This CMake file is the default used by apps and is placed inside react-native
# to encapsulate it from user space (so you won't need to touch C++/Cmake code at all on Android).
#
# If you wish to customize it (because you want to manually link a C++ library or pass a custom
# compilation flag) you can:
#
# 1. Copy this CMake file inside the `android/app/src/main/jni` folder of your project
# 2. Copy the OnLoad.cpp (in this same folder) file inside the same folder as above.
# 3. Extend your `android/app/build.gradle` as follows
#
# android {
# // Other config here...
# externalNativeBuild {
# cmake {
# path "src/main/jni/CMakeLists.txt"
# }
# }
# }
cmake_minimum_required(VERSION 3.13)
# Define the library name here.
project(appmodules)
# This file includes all the necessary to let you build your application with the New Architecture.
include(${REACT_ANDROID_DIR}/cmake-utils/ReactNative-application.cmake)

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.
*/
// This C++ file is part of the default configuration used by apps and is placed
// inside react-native to encapsulate it from user space (so you won't need to
// touch C++/Cmake code at all on Android).
//
// If you wish to customize it (because you want to manually link a C++ library
// or pass a custom compilation flag) you can:
//
// 1. Copy this CMake file inside the `android/app/src/main/jni` folder of your
// project
// 2. Copy the OnLoad.cpp (in this same folder) file inside the same folder as
// above.
// 3. Extend your `android/app/build.gradle` as follows
//
// android {
// // Other config here...
// externalNativeBuild {
// cmake {
// path "src/main/jni/CMakeLists.txt"
// }
// }
// }
#include <DefaultComponentsRegistry.h>
#include <DefaultTurboModuleManagerDelegate.h>
#include <FBReactNativeSpec.h>
#include <autolinking.h>
#include <fbjni/fbjni.h>
#include <react/renderer/componentregistry/ComponentDescriptorProviderRegistry.h>
#ifdef REACT_NATIVE_APP_CODEGEN_HEADER
#include REACT_NATIVE_APP_CODEGEN_HEADER
#endif
#ifdef REACT_NATIVE_APP_COMPONENT_DESCRIPTORS_HEADER
#include REACT_NATIVE_APP_COMPONENT_DESCRIPTORS_HEADER
#endif
namespace facebook::react {
void registerComponents(
std::shared_ptr<const ComponentDescriptorProviderRegistry> registry) {
// Custom Fabric Components go here. You can register custom
// components coming from your App or from 3rd party libraries here.
//
// providerRegistry->add(concreteComponentDescriptorProvider<
// MyComponentDescriptor>());
// We link app local components if available
#ifdef REACT_NATIVE_APP_COMPONENT_REGISTRATION
REACT_NATIVE_APP_COMPONENT_REGISTRATION(registry);
#endif
// And we fallback to the components autolinked
autolinking_registerProviders(registry);
}
std::shared_ptr<TurboModule> cxxModuleProvider(
const std::string& name,
const std::shared_ptr<CallInvoker>& jsInvoker) {
// Here you can provide your CXX Turbo Modules coming from
// either your application or from external libraries. The approach to follow
// is similar to the following (for a module called `NativeCxxModuleExample`):
//
// if (name == NativeCxxModuleExample::kModuleName) {
// return std::make_shared<NativeCxxModuleExample>(jsInvoker);
// }
// And we fallback to the CXX module providers autolinked
return autolinking_cxxModuleProvider(name, jsInvoker);
return nullptr;
}
std::shared_ptr<TurboModule> javaModuleProvider(
const std::string& name,
const JavaTurboModule::InitParams& params) {
// Here you can provide your own module provider for TurboModules coming from
// either your application or from external libraries. The approach to follow
// is similar to the following (for a library called `samplelibrary`):
//
// auto module = samplelibrary_ModuleProvider(name, params);
// if (module != nullptr) {
// return module;
// }
// return rncore_ModuleProvider(name, params);
// We link app local modules if available
#ifdef REACT_NATIVE_APP_MODULE_PROVIDER
auto module = REACT_NATIVE_APP_MODULE_PROVIDER(name, params);
if (module != nullptr) {
return module;
}
#endif
// We first try to look up core modules
if (auto module = FBReactNativeSpec_ModuleProvider(name, params)) {
return module;
}
// And we fallback to the module providers autolinked
if (auto module = autolinking_ModuleProvider(name, params)) {
return module;
}
return nullptr;
}
} // namespace facebook::react
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) {
return facebook::jni::initialize(vm, [] {
facebook::react::DefaultTurboModuleManagerDelegate::cxxModuleProvider =
&facebook::react::cxxModuleProvider;
facebook::react::DefaultTurboModuleManagerDelegate::javaModuleProvider =
&facebook::react::javaModuleProvider;
facebook::react::DefaultComponentsRegistry::
registerComponentDescriptorsFromEntryPoint =
&facebook::react::registerComponents;
});
}

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.
cmake_minimum_required(VERSION 3.13)
set(CMAKE_VERBOSE_MAKEFILE on)
# This CMake file exposes the Folly Flags that all the libraries should use when
# compiling/linking against a dependency which requires folly.
SET(folly_FLAGS
-DFOLLY_NO_CONFIG=1
-DFOLLY_HAVE_CLOCK_GETTIME=1
-DFOLLY_USE_LIBCPP=1
-DFOLLY_CFG_NO_COROUTINES=1
-DFOLLY_MOBILE=1
-DFOLLY_HAVE_RECVMMSG=1
-DFOLLY_HAVE_PTHREAD=1
# Once we target android-23 above, we can comment
# the following line. NDK uses GNU style stderror_r() after API 23.
-DFOLLY_HAVE_XSI_STRERROR_R=1
)

View File

@@ -0,0 +1,108 @@
/*
* 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.
*/
import org.jetbrains.kotlin.gradle.plugin.extraProperties
plugins { id("maven-publish") }
group = "com.facebook.react"
version =
parent?.extraProperties?.get("publishing_version")
?: error("publishing_version not set for external-artifacts")
configurations.maybeCreate("externalArtifacts")
// [iOS] React Native Dependencies
val reactNativeDependenciesDebugArtifactFile: RegularFile =
layout.projectDirectory.file("artifacts/ReactNativeDependenciesDebug.xcframework.tar.gz")
val reactNativeDependenciesDebugArtifact: PublishArtifact =
artifacts.add("externalArtifacts", reactNativeDependenciesDebugArtifactFile) {
type = "tgz"
extension = "tar.gz"
classifier = "reactnative-dependencies-debug"
}
val reactNativeDependenciesReleaseArtifactFile: RegularFile =
layout.projectDirectory.file("artifacts/ReactNativeDependenciesRelease.xcframework.tar.gz")
val reactNativeDependenciesReleaseArtifact: PublishArtifact =
artifacts.add("externalArtifacts", reactNativeDependenciesReleaseArtifactFile) {
type = "tgz"
extension = "tar.gz"
classifier = "reactnative-dependencies-release"
}
val reactNativeDependenciesDebugDSYMArtifactFile: RegularFile =
layout.projectDirectory.file("artifacts/ReactNativeDependenciesDebug.framework.dSYM.tar.gz")
val reactNativeDependenciesDebugDSYMArtifact: PublishArtifact =
artifacts.add("externalArtifacts", reactNativeDependenciesDebugDSYMArtifactFile) {
type = "tgz"
extension = "tar.gz"
classifier = "reactnative-dependencies-dSYM-debug"
}
val reactNativeDependenciesReleaseDSYMArtifactFile: RegularFile =
layout.projectDirectory.file("artifacts/ReactNativeDependenciesRelease.framework.dSYM.tar.gz")
val reactNativeDependenciesReleaseDSYMArtifact: PublishArtifact =
artifacts.add("externalArtifacts", reactNativeDependenciesReleaseDSYMArtifactFile) {
type = "tgz"
extension = "tar.gz"
classifier = "reactnative-dependencies-dSYM-release"
}
// [iOS] React Native Core
val reactCoreDebugArtifactFile: RegularFile =
layout.projectDirectory.file("artifacts/ReactCoreDebug.xcframework.tar.gz")
val reactCoreDebugArtifact: PublishArtifact =
artifacts.add("externalArtifacts", reactCoreDebugArtifactFile) {
type = "tgz"
extension = "tar.gz"
classifier = "reactnative-core-debug"
}
val reactCoreReleaseArtifactFile: RegularFile =
layout.projectDirectory.file("artifacts/ReactCoreRelease.xcframework.tar.gz")
val reactCoreReleaseArtifact: PublishArtifact =
artifacts.add("externalArtifacts", reactCoreReleaseArtifactFile) {
type = "tgz"
extension = "tar.gz"
classifier = "reactnative-core-release"
}
val reactCoreDebugDSYMArtifactFile: RegularFile =
layout.projectDirectory.file("artifacts/ReactCoreDebug.framework.dSYM.tar.gz")
val reactCoreDebugDSYMArtifact: PublishArtifact =
artifacts.add("externalArtifacts", reactCoreDebugDSYMArtifactFile) {
type = "tgz"
extension = "tar.gz"
classifier = "reactnative-core-dSYM-debug"
}
val reactCoreReleaseDSYMArtifactFile: RegularFile =
layout.projectDirectory.file("artifacts/ReactCoreRelease.framework.dSYM.tar.gz")
val reactCoreReleaseDSYMArtifact: PublishArtifact =
artifacts.add("externalArtifacts", reactCoreReleaseDSYMArtifactFile) {
type = "tgz"
extension = "tar.gz"
classifier = "reactnative-core-dSYM-release"
}
apply(from = "../publish.gradle")
publishing {
publications {
getByName("release", MavenPublication::class) {
artifactId = "react-native-artifacts"
artifact(reactNativeDependenciesDebugArtifact)
artifact(reactNativeDependenciesReleaseArtifact)
artifact(reactNativeDependenciesDebugDSYMArtifact)
artifact(reactNativeDependenciesReleaseDSYMArtifact)
artifact(reactCoreDebugArtifact)
artifact(reactCoreReleaseArtifact)
artifact(reactCoreDebugDSYMArtifact)
artifact(reactCoreReleaseDSYMArtifact)
}
}
}

View File

@@ -0,0 +1,40 @@
VERSION_NAME=0.83.2
react.internal.publishingGroup=com.facebook.react
react.internal.hermesPublishingGroup=com.facebook.hermes
android.useAndroidX=true
# We want to have more fine grained control on the Java version for
# ReactAndroid, therefore we disable RGNP Java version alignment mechanism
react.internal.disableJavaVersionAlignment=true
# Binary Compatibility Validator properties
# We ignore:
# - BuildConfig classes because they are generated and not part of the public API
binaryCompatibilityValidator.ignoredClasses=com.facebook.react.BuildConfig,\
com.facebook.react.views.progressbar.ReactProgressBarViewManager$$PropsSetter,\
com.facebook.react.views.progressbar.ProgressBarShadowNode$$PropsSetter
binaryCompatibilityValidator.ignoredPackages=com.facebook.debug,\
com.facebook.fbreact,\
com.facebook.hermes,\
com.facebook.perftest,\
com.facebook.proguard,\
com.facebook.react.bridgeless.internal,\
com.facebook.react.common.annotations,\
com.facebook.react.fabric.internal.interop,\
com.facebook.react.flipper,\
com.facebook.react.internal,\
com.facebook.react.module.processing,\
com.facebook.react.processing,\
com.facebook.react.runtime.internal,\
com.facebook.react.uimanager.internal,\
com.facebook.react.views.text.internal,\
com.facebook.systrace,\
com.facebook.yoga
binaryCompatibilityValidator.nonPublicMarkers=com.facebook.react.common.annotations.VisibleForTesting,\
com.facebook.react.common.annotations.UnstableReactNativeAPI,\
com.facebook.react.common.annotations.FrameworkAPI
binaryCompatibilityValidator.validationDisabled=true
binaryCompatibilityValidator.outputApiFileName=ReactAndroid

View File

@@ -0,0 +1,392 @@
/*
* 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.
*/
import com.facebook.react.internal.PrivateReactExtension
import com.facebook.react.tasks.internal.*
import de.undercouch.gradle.tasks.download.Download
import org.apache.tools.ant.taskdefs.condition.Os
plugins {
id("maven-publish")
id("signing")
alias(libs.plugins.android.library)
alias(libs.plugins.download)
}
group = "com.facebook.react"
version = parent?.properties?.get("publishing_version")?.toString()!!
val cmakeVersion = parent?.properties?.get("cmake_version")?.toString()!!
val cmakePath = "${getSDKPath()}/cmake/$cmakeVersion"
val cmakeBinaryPath = "${cmakePath}/bin/cmake"
fun getSDKPath(): String {
val androidSdkRoot = System.getenv("ANDROID_SDK_ROOT")
val androidHome = System.getenv("ANDROID_HOME")
return when {
!androidSdkRoot.isNullOrBlank() -> androidSdkRoot
!androidHome.isNullOrBlank() -> androidHome
else -> throw IllegalStateException("Neither ANDROID_SDK_ROOT nor ANDROID_HOME is set.")
}
}
fun getSDKManagerPath(): String {
val metaSdkManagerPath = File("${getSDKPath()}/cmdline-tools/latest/bin/sdkmanager")
val ossSdkManagerPath = File("${getSDKPath()}/tools/bin/sdkmanager")
val windowsMetaSdkManagerPath = File("${getSDKPath()}/cmdline-tools/latest/bin/sdkmanager.bat")
val windowsOssSdkManagerPath = File("${getSDKPath()}/tools/bin/sdkmanager.bat")
val linuxSdkManagerPath = File("${getSDKPath()}/cmdline-tools/tools/bin/sdkmanager")
return when {
metaSdkManagerPath.exists() -> metaSdkManagerPath.absolutePath
windowsMetaSdkManagerPath.exists() -> windowsMetaSdkManagerPath.absolutePath
ossSdkManagerPath.exists() -> ossSdkManagerPath.absolutePath
windowsOssSdkManagerPath.exists() -> windowsOssSdkManagerPath.absolutePath
linuxSdkManagerPath.exists() -> linuxSdkManagerPath.absolutePath
else -> throw GradleException("Could not find sdkmanager executable.")
}
}
val hermesV1Enabled =
rootProject.extensions.getByType(PrivateReactExtension::class.java).hermesV1Enabled.get()
val reactNativeRootDir = project(":packages:react-native:ReactAndroid").projectDir.parent
val customDownloadDir = System.getenv("REACT_NATIVE_DOWNLOADS_DIR")
val downloadsDir =
if (customDownloadDir != null) {
File(customDownloadDir)
} else {
File(reactNativeRootDir, "sdks/download")
}
// By default we are going to download and unzip hermes inside the /sdks/hermes folder
// but you can provide an override for where the hermes source code is located.
val buildDir = project.layout.buildDirectory.get().asFile
val overrideHermesDir = System.getenv("REACT_NATIVE_OVERRIDE_HERMES_DIR") != null
val hermesDir =
if (overrideHermesDir) {
File(System.getenv("REACT_NATIVE_OVERRIDE_HERMES_DIR"))
} else {
File(reactNativeRootDir, "sdks/hermes")
}
val hermesBuildDir = File("$buildDir/hermes")
val hermesCOutputBinary = File("$buildDir/hermes/bin/hermesc")
// This filetree represents the file of the Hermes build that we want as input/output
// of the buildHermesC task. Gradle will compute the hash of files in the file tree
// and won't rebuilt hermesc unless those files are changing.
val hermesBuildOutputFileTree =
fileTree(hermesBuildDir.toString())
.include("**/*.cmake", "**/*.marks", "**/compiler_depends.ts", "**/Makefile", "**/link.txt")
val hermesVersionProvider: Provider<String> =
providers.provider {
var hermesVersion = if (hermesV1Enabled) "250829098.0.0-stable" else "main"
val hermesVersionFile =
File(
reactNativeRootDir,
if (hermesV1Enabled) "sdks/.hermesv1version" else "sdks/.hermesversion",
)
if (hermesVersionFile.exists()) {
hermesVersion = hermesVersionFile.readText()
}
hermesVersion
}
val ndkBuildJobs = Runtime.getRuntime().availableProcessors().toString()
val prefabHeadersDir = File("$buildDir/prefab-headers")
// We inject the JSI directory used inside the Hermes build with the -DJSI_DIR config.
val jsiDir = File(reactNativeRootDir, "ReactCommon/jsi")
val downloadHermesDest = File(downloadsDir, "hermes.tar.gz")
val downloadHermes by
tasks.registering(Download::class) {
src(
providers.provider {
"https://github.com/facebook/hermes/tarball/${hermesVersionProvider.get()}"
}
)
onlyIfModified(true)
overwrite(true)
quiet(true)
useETag("all")
retries(5)
dest(downloadHermesDest)
}
val unzipHermes by
tasks.registering(Copy::class) {
dependsOn(downloadHermes)
from(tarTree(downloadHermesDest)) {
eachFile {
// We flatten the unzip as the tarball contains a `facebook-hermes-<SHA>`
// folder at the top level.
if (this.path.startsWith("facebook-hermes-")) {
this.path = this.path.substringAfter("/")
}
}
}
into(hermesDir)
}
// NOTE: ideally, we would like CMake to be installed automatically by the `externalNativeBuild`
// below. To do that, we would need the various `ConfigureCMake*` tasks to run *before*
// `configureBuildForHermes` and `buildHermesC` so that CMake is available for their run. But the
// `ConfigureCMake*` tasks depend upon the `ImportHostCompilers.cmake` file which is actually
// generated by
// the two tasks mentioned before, so we install CMake manually to break the circular dependency.
val installCMake by
tasks.registering(CustomExecTask::class) {
onlyIfProvidedPathDoesNotExists.set(cmakePath)
commandLine(
windowsAwareCommandLine(getSDKManagerPath(), "--install", "cmake;${cmakeVersion}")
)
}
val configureBuildForHermes by
tasks.registering(CustomExecTask::class) {
dependsOn(installCMake)
workingDir(hermesDir)
inputs.dir(hermesDir)
outputs.files(hermesBuildOutputFileTree)
var cmakeCommandLine =
windowsAwareCommandLine(
cmakeBinaryPath,
// Suppress all warnings as this is the Hermes build and we can't fix them.
"--log-level=ERROR",
"-Wno-dev",
"-S",
".",
"-B",
hermesBuildDir.toString(),
"-DJSI_DIR=" + jsiDir.absolutePath,
"-DCMAKE_BUILD_TYPE=Release",
)
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
cmakeCommandLine = cmakeCommandLine + "-GNMake Makefiles"
}
commandLine(cmakeCommandLine)
standardOutputFile.set(project.file("$buildDir/configure-hermesc.log"))
}
val buildHermesC by
tasks.registering(CustomExecTask::class) {
dependsOn(configureBuildForHermes)
workingDir(hermesDir)
inputs.files(hermesBuildOutputFileTree)
outputs.file(hermesCOutputBinary)
commandLine(
cmakeBinaryPath,
"--build",
hermesBuildDir.toString(),
"--target",
"hermesc",
"-j",
ndkBuildJobs,
)
standardOutputFile.set(project.file("$buildDir/build-hermesc.log"))
errorOutputFile.set(project.file("$buildDir/build-hermesc.error.log"))
}
val prepareHeadersForPrefab by
tasks.registering(Copy::class) {
dependsOn(buildHermesC)
from("$hermesDir/API")
from("$hermesDir/public")
include("**/*.h")
exclude("jsi/**")
into(prefabHeadersDir)
}
val buildHermesLib by
tasks.registering(CustomExecTask::class) {
dependsOn(buildHermesC)
workingDir(hermesDir)
inputs.files(hermesBuildOutputFileTree)
commandLine(
cmakeBinaryPath,
"--build",
hermesBuildDir.toString(),
"--target",
"hermesvm",
"-j",
ndkBuildJobs,
)
standardOutputFile.set(project.file("$buildDir/build-hermes-lib.log"))
errorOutputFile.set(project.file("$buildDir/build-hermes-lib.error.log"))
}
fun windowsAwareCommandLine(vararg commands: String): List<String> {
val result =
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
mutableListOf("cmd", "/c")
} else {
mutableListOf()
}
result.addAll(commands)
return result
}
fun reactNativeArchitectures(): List<String> {
val value = project.properties["reactNativeArchitectures"]
return value?.toString()?.split(",") ?: listOf("armeabi-v7a", "x86", "x86_64", "arm64-v8a")
}
repositories {
// Normally RNGP will set repositories for all modules,
// but when consumed from source, we need to re-declare
// those repositories as there is no app module there.
mavenCentral()
google()
}
android {
compileSdk = libs.versions.compileSdk.get().toInt()
buildToolsVersion = libs.versions.buildTools.get()
namespace = "com.facebook.hermes"
// Used to override the NDK path/version on internal CI or by allowing
// users to customize the NDK path/version from their root project (e.g. for Apple Silicon
// support)
if (rootProject.hasProperty("ndkPath") && rootProject.properties["ndkPath"] != null) {
ndkPath = rootProject.properties["ndkPath"].toString()
}
if (rootProject.hasProperty("ndkVersion") && rootProject.properties["ndkVersion"] != null) {
ndkVersion = rootProject.properties["ndkVersion"].toString()
} else {
ndkVersion = libs.versions.ndkVersion.get()
}
defaultConfig {
minSdk = libs.versions.minSdk.get().toInt()
externalNativeBuild {
cmake {
arguments(
"--log-level=ERROR",
"-Wno-dev",
"-DHERMES_IS_ANDROID=True",
"-DANDROID_STL=c++_shared",
"-DANDROID_PIE=True",
"-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON",
"-DIMPORT_HOST_COMPILERS=${File(hermesBuildDir, "ImportHostCompilers.cmake").toString()}",
"-DJSI_DIR=${jsiDir}",
"-DHERMES_BUILD_SHARED_JSI=True",
"-DHERMES_RELEASE_VERSION=for RN ${version}",
"-DCMAKE_INTERPROCEDURAL_OPTIMIZATION=True",
// We intentionally build Hermes with Intl support only. This is to simplify
// the build setup and to avoid overcomplicating the build-type matrix.
"-DHERMES_ENABLE_INTL=True",
)
if (hermesV1Enabled) {
arguments("-DHERMESVM_HEAP_HV_MODE=HEAP_HV_PREFER32")
}
targets("hermesvm")
}
}
ndk { abiFilters.addAll(reactNativeArchitectures()) }
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
}
externalNativeBuild {
cmake {
version = cmakeVersion
path = File("$hermesDir/CMakeLists.txt")
}
}
buildTypes {
debug {
externalNativeBuild {
cmake {
// JS developers aren't VM developers.
// Therefore we're passing as build type Release, to provide a faster build.
// This has the (unlucky) side effect of letting AGP call the build
// tasks `configureCMakeRelease` while is actually building the debug flavor.
arguments(
"-DCMAKE_BUILD_TYPE=Release",
// For debug builds, explicitly enable the Hermes Debugger.
"-DHERMES_ENABLE_DEBUGGER=True",
)
}
}
}
release {
externalNativeBuild {
cmake {
arguments(
"-DCMAKE_BUILD_TYPE=MinSizeRel",
// For release builds, we don't want to enable the Hermes Debugger.
"-DHERMES_ENABLE_DEBUGGER=False",
)
}
}
}
buildTypes {
create("debugOptimized") {
initWith(getByName("debug"))
externalNativeBuild { cmake { arguments("-DCMAKE_BUILD_TYPE=Release") } }
}
}
}
sourceSets.getByName("main") {
manifest.srcFile("$hermesDir/android/hermes/src/main/AndroidManifest.xml")
java.srcDirs("$hermesDir/lib/Platform/Intl/java", "$hermesDir/lib/Platform/Unicode/java")
}
buildFeatures {
prefab = true
prefabPublishing = true
}
dependencies {
implementation(libs.fbjni)
implementation(libs.yoga.proguard.annotations)
implementation(libs.androidx.annotation)
}
packaging {
jniLibs.excludes.add("**/libc++_shared.so")
jniLibs.excludes.add("**/libjsi.so")
jniLibs.excludes.add("**/libfbjni.so")
}
publishing {
multipleVariants {
withSourcesJar()
allVariants()
}
}
prefab { create("hermesvm") { headers = prefabHeadersDir.absolutePath } }
}
afterEvaluate {
if (!overrideHermesDir) {
// If you're not specifying a Hermes Path override, we want to
// download/unzip Hermes from Github then.
tasks.getByName("configureBuildForHermes").dependsOn(unzipHermes)
tasks.getByName("prepareHeadersForPrefab").dependsOn(unzipHermes)
}
tasks.getByName("preBuild").dependsOn(buildHermesC)
tasks.getByName("preBuild").dependsOn(prepareHeadersForPrefab)
}
tasks.withType<JavaCompile>().configureEach {
options.compilerArgs.add("-Xlint:deprecation,unchecked")
options.compilerArgs.add("-Werror")
}

View File

@@ -0,0 +1 @@
android.disableAutomaticComponentCreation=true

View File

@@ -0,0 +1,75 @@
# 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.
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# Disabling obfuscation is useful if you collect stack traces from production crashes
# (unless you are using a system that supports de-obfuscate the stack traces).
# -dontobfuscate
# React Native
# Keep our interfaces so they can be used by other ProGuard rules.
# See http://sourceforge.net/p/proguard/bugs/466/
-keep,allowobfuscation @interface com.facebook.proguard.annotations.DoNotStrip
# Do not strip any method/class that is annotated with @DoNotStrip
-keep @com.facebook.proguard.annotations.DoNotStrip class *
-keepclassmembers class * {
@com.facebook.proguard.annotations.DoNotStrip *;
}
-keep @com.facebook.proguard.annotations.DoNotStripAny class * {
*;
}
-keep @com.facebook.jni.annotations.DoNotStrip class *
-keepclassmembers class * {
@com.facebook.jni.annotations.DoNotStrip *;
}
-keep @com.facebook.jni.annotations.DoNotStripAny class * {
*;
}
-keep class * implements com.facebook.react.bridge.JavaScriptModule { *; }
-keep class * implements com.facebook.react.bridge.NativeModule { *; }
-keepclassmembers,includedescriptorclasses class * { native <methods>; }
-keepclassmembers class * { @com.facebook.react.uimanager.annotations.ReactProp <methods>; }
-keepclassmembers class * { @com.facebook.react.uimanager.annotations.ReactPropGroup <methods>; }
-dontwarn com.facebook.react.**
-keep,includedescriptorclasses class com.facebook.react.bridge.** { *; }
-keep,includedescriptorclasses class com.facebook.react.turbomodule.core.** { *; }
-keep,includedescriptorclasses class com.facebook.react.internal.turbomodule.core.** { *; }
# hermes
-keep class com.facebook.jni.** { *; }
# okio
-keep class sun.misc.Unsafe { *; }
-dontwarn java.nio.file.*
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
-dontwarn okio.**
# yoga
-keep,allowobfuscation @interface com.facebook.yoga.annotations.DoNotStrip
-keep @com.facebook.yoga.annotations.DoNotStrip class *
-keepclassmembers class * {
@com.facebook.yoga.annotations.DoNotStrip *;
}
# fresco
-keep public class com.facebook.imageutils.** {
public *;
}

82
node_modules/react-native/ReactAndroid/publish.gradle generated vendored Normal file
View File

@@ -0,0 +1,82 @@
/*
* 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.
*/
apply plugin: 'maven-publish'
apply plugin: 'signing'
def isSnapshot = findProperty("isSnapshot")?.toBoolean()
def signingKey = findProperty("SIGNING_KEY")
def signingPwd = findProperty("SIGNING_PWD")
def reactAndroidProjectDir = project(':packages:react-native:ReactAndroid').projectDir
def mavenTempLocalUrl = "file:///tmp/maven-local"
publishing {
publications {
release(MavenPublication) {
afterEvaluate {
// We do a multi variant release, so for Android libraries
// we publish `components.release`
if (plugins.hasPlugin("com.android.library")) {
from components.default
}
}
// We populate the publishing version using the project version,
// appending -SNAPSHOT if on nightly or prerelase.
if (isSnapshot) {
version = this.version + "-SNAPSHOT"
} else {
version = this.version
}
pom {
name = "react-native"
description = "A framework for building native apps with React"
url = "https://github.com/facebook/react-native"
developers {
developer {
id = "facebook"
name = "Facebook"
}
}
licenses {
license {
name = "MIT License"
url = "https://github.com/facebook/react-native/blob/HEAD/LICENSE"
distribution = "repo"
}
}
scm {
url = "https://github.com/facebook/react-native.git"
connection = "scm:git:https://github.com/facebook/react-native.git"
developerConnection = "scm:git:git@github.com:facebook/react-native.git"
}
}
}
}
repositories {
maven {
name = "mavenTempLocal"
url = mavenTempLocalUrl
}
}
if (signingKey && signingPwd) {
logger.info("PGP Key found - Signing enabled")
signing {
useInMemoryPgpKeys(signingKey, signingPwd)
sign(publishing.publications.release)
}
} else {
logger.info("Signing disabled as the PGP key was not found")
}
}

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!--
This manifest file is used only by Gradle to configure debug-only capabilities
for React Native Apps.
-->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<application>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity"
android:exported="false" />
</application>
</manifest>

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,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.debug.debugoverlay.model
/**
* Tag for a debug overlay log message. Name must be unique.
*
* @param name Name of tag.
* @param description Description to display in settings.
* @param color Color for tag display.
*/
internal class DebugOverlayTag(
val name: String,
val description: String,
val color: Int,
)

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.debug.holder
import com.facebook.debug.debugoverlay.model.DebugOverlayTag
/** No-op implementation of [Printer]. */
internal object NoopPrinter : Printer {
override fun logMessage(tag: DebugOverlayTag, message: String, vararg args: Any?): Unit = Unit
override fun logMessage(tag: DebugOverlayTag, message: String): Unit = Unit
override fun shouldDisplayLogMessage(tag: DebugOverlayTag): Boolean = false
}

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.debug.holder
import com.facebook.debug.debugoverlay.model.DebugOverlayTag
/** Interface to debugging tool. */
internal interface Printer {
fun logMessage(tag: DebugOverlayTag, message: String, vararg args: Any?)
fun logMessage(tag: DebugOverlayTag, message: String)
fun shouldDisplayLogMessage(tag: DebugOverlayTag): Boolean
}

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.debug.holder
/** Holder for debugging tool instance. */
internal object PrinterHolder {
@JvmStatic var printer: Printer = NoopPrinter
}

View File

@@ -0,0 +1,37 @@
/*
* 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.debug.tags
import android.graphics.Color
import com.facebook.debug.debugoverlay.model.DebugOverlayTag
/** Category for debug overlays. */
internal object ReactDebugOverlayTags {
@JvmField
val PERFORMANCE: DebugOverlayTag =
DebugOverlayTag("Performance", "Markers for Performance", Color.GREEN)
@JvmField
val NAVIGATION: DebugOverlayTag =
DebugOverlayTag("Navigation", "Tag for navigation", Color.rgb(0x9C, 0x27, 0xB0))
@JvmField
val RN_CORE: DebugOverlayTag =
DebugOverlayTag("RN Core", "Tag for React Native Core", Color.BLACK)
@JvmField
val BRIDGE_CALLS: DebugOverlayTag =
DebugOverlayTag("Bridge Calls", "JS to Java calls (warning: this is spammy)", Color.MAGENTA)
@JvmField
val NATIVE_MODULE: DebugOverlayTag =
DebugOverlayTag("Native Module", "Native Module init", Color.rgb(0x80, 0x00, 0x80))
@JvmField
val UI_MANAGER: DebugOverlayTag =
DebugOverlayTag(
"UI Manager",
"UI Manager View Operations (requires restart\nwarning: this is spammy)",
Color.CYAN,
)
}

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,146 @@
/*
* 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.bridge.ReactMarker
import com.facebook.react.bridge.ReactMarkerConstants
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.module.model.ReactModuleInfo
import com.facebook.react.module.model.ReactModuleInfoProvider
import com.facebook.react.uimanager.ViewManager
import com.facebook.systrace.Systrace.TRACE_TAG_REACT
import com.facebook.systrace.SystraceMessage
/** React package supporting lazy creation of native modules. */
@Deprecated("This class is deprecated, please use BaseReactPackage instead.")
@LegacyArchitecture(logLevel = LegacyArchitectureLogLevel.ERROR)
public abstract class LazyReactPackage : ReactPackage {
/**
* We return an iterable
*
* @param reactContext context
* @return An [Iterable]<[ModuleHolder]> that contains all native modules registered for the
* context
*/
public fun getNativeModuleIterator(
reactContext: ReactApplicationContext
): Iterable<ModuleHolder> {
val reactModuleInfoMap: Map<String, ReactModuleInfo> =
reactModuleInfoProvider.getReactModuleInfos()
val nativeModules = getNativeModules(reactContext)
return object : Iterable<ModuleHolder> {
override fun iterator(): Iterator<ModuleHolder> {
var position = 0
return object : Iterator<ModuleHolder> {
override fun hasNext(): Boolean = position < nativeModules.size
override fun next(): ModuleHolder {
val moduleSpec = nativeModules[position++]
val name = moduleSpec.name
val reactModuleInfo = reactModuleInfoMap[name]
return if (reactModuleInfo == null) {
val module: NativeModule
ReactMarker.logMarker(
ReactMarkerConstants.CREATE_MODULE_START,
name,
)
try {
module = moduleSpec.provider.get()
} finally {
ReactMarker.logMarker(ReactMarkerConstants.CREATE_MODULE_END)
}
ModuleHolder(module)
} else {
ModuleHolder(reactModuleInfo, moduleSpec.provider)
}
}
}
}
}
}
/**
* @param reactContext react application context that can be used to create modules
* @return list of module specs that can create the native modules
*/
protected abstract fun getNativeModules(reactContext: ReactApplicationContext): List<ModuleSpec>
/**
* Internal accessor to [getNativeModules]. This is needed because [getNativeModules] was
* originally protected in Java (which had subclass + package visibility) and is now protected in
* Kotlin (which has only subclass visiblity). We add this accessor to prevent making
* [getNativeModules] public
*/
internal fun internal_getNativeModules(reactContext: ReactApplicationContext): List<ModuleSpec> =
getNativeModules(reactContext)
/**
* @param reactContext react application context that can be used to create modules
* @return A [List]<[NativeModule]> to register
*/
@Suppress("DEPRECATION")
@Deprecated("Migrate to [BaseReactPackage] and implement [getModule] instead.")
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> =
buildList {
for (holder in getNativeModules(reactContext)) {
val nativeModule: NativeModule
SystraceMessage.beginSection(TRACE_TAG_REACT, "createNativeModule").flush()
ReactMarker.logMarker(ReactMarkerConstants.CREATE_MODULE_START, holder.name)
try {
nativeModule = holder.provider.get()
} finally {
ReactMarker.logMarker(ReactMarkerConstants.CREATE_MODULE_END)
SystraceMessage.endSection(TRACE_TAG_REACT).flush()
}
add(nativeModule)
}
}
/**
* @param reactContext react application context that can be used to create View Managers.
* @return list of module specs that can create the View Managers.
*/
public 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.isEmpty()) {
return emptyList()
}
val viewManagers: List<ViewManager<in Nothing, in Nothing>> = buildList {
for (moduleSpec in viewManagerModuleSpecs) {
add(moduleSpec.provider.get() as ViewManager<in Nothing, in Nothing>)
}
}
return viewManagers
}
public abstract val reactModuleInfoProvider: ReactModuleInfoProvider
private companion object {
init {
LegacyArchitectureLogger.assertLegacyArchitecture(
"LazyReactPackage",
LegacyArchitectureLogLevel.ERROR,
)
}
}
}

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,81 @@
/*
* 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 =
@Suppress("DEPRECATION")
if (reactPackage is LazyReactPackage) {
reactPackage.getNativeModuleIterator(reactApplicationContext)
} else 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,182 @@
/*
* 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();
}
@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,445 @@
/*
* 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
mainComponentName = appKey
this.launchOptions = launchOptions
doubleTapReloadRecognizer = DoubleTapReloadRecognizer()
this.reactNativeHost = reactNativeHost
}
public constructor(
activity: Activity,
reactHost: ReactHost?,
appKey: String?,
launchOptions: Bundle?,
) {
this.activity = activity
mainComponentName = appKey
this.launchOptions = launchOptions
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,
) {
isFabricEnabled = fabricEnabled
this.activity = activity
mainComponentName = appKey
this.launchOptions = launchOptions
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
) {
reactHost?.onBackPressed()
return 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,409 @@
/*
* 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.NotThreadSafeBridgeIdleDebugListener
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 bridgeIdleDebugListener: NotThreadSafeBridgeIdleDebugListener? = 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
}
public fun setBridgeIdleDebugListener(
bridgeIdleDebugListener: NotThreadSafeBridgeIdleDebugListener
): ReactInstanceManagerBuilder {
this.bridgeIdleDebugListener = bridgeIdleDebugListener
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,
bridgeIdleDebugListener,
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 {
init {
LegacyArchitectureLogger.assertLegacyArchitecture(
"ReactInstanceManagerBuilder",
LegacyArchitectureLogLevel.ERROR,
)
}
private val TAG: String = ReactInstanceManagerBuilder::class.java.simpleName
}
}

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 LazyReactPackage, 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,250 @@
/*
* 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.CxxModuleWrapper
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.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
import javax.inject.Provider
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
}
@Suppress("DEPRECATION")
if (shouldSupportLegacyPackages() && reactPackage is LazyReactPackage) {
// TODO(T145105887): Output warnings that LazyReactPackage was used
val lazyPkg = reactPackage
val moduleSpecs: List<ModuleSpec> =
lazyPkg.internal_getNativeModules(reactApplicationContext)
val moduleSpecProviderMap: MutableMap<String?, Provider<out NativeModule>> = mutableMapOf()
for (moduleSpec in moduleSpecs) {
moduleSpecProviderMap[moduleSpec.getName()] = moduleSpec.getProvider()
}
val moduleProvider = ModuleProvider { moduleName: String ->
moduleSpecProviderMap[moduleName]?.get()
}
moduleProviders.add(moduleProvider)
packageModuleInfos[moduleProvider] = lazyPkg.reactModuleInfoProvider.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,
reactModule.isCxxModule,
ReactModuleInfo.classIsTurboModule(moduleClass),
)
else
ReactModuleInfo(
moduleName,
moduleClass.name,
module.canOverrideExistingModule(),
true,
CxxModuleWrapper::class.java.isAssignableFrom(moduleClass),
ReactModuleInfo.classIsTurboModule(moduleClass),
)
reactModuleInfoMap[moduleName] = moduleInfo
moduleMap[moduleName] = module
}
val moduleProvider = ModuleProvider { module -> moduleMap[module] }
moduleProviders.add(moduleProvider)
packageModuleInfos[moduleProvider] = reactModuleInfoMap
}
}
}
override fun unstable_shouldEnableLegacyModuleInterop(): Boolean = shouldEnableLegacyModuleInterop
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 (!unstable_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 = unstable_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,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
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.uimanager.ViewManager
public interface ViewManagerOnDemandReactPackage {
/**
* Provides a list of names of ViewManagers with which these modules can be accessed from JS.
* Typically, this is ViewManager.getName().
*/
public fun getViewManagerNames(reactContext: ReactApplicationContext): Collection<String>
/**
* Creates and returns a ViewManager with a specific name {@param viewManagerName}. It's up to an
* implementing package how to interpret the name.
*/
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,41 @@
/*
* 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 ${javaClass.simpleName} 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
init {
onUpdateConfig(config)
}
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)
}
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
}
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)
}
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
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)
}
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()
}
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,100 @@
/*
* 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.WritableArray
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 {
@Deprecated(
"Deprecated in Java",
ReplaceWith("receiveEvent(surfaceId, targetTag, eventName, params)"),
)
override fun receiveEvent(targetTag: Int, eventName: String, params: WritableMap?) =
receiveEvent(-1, targetTag, eventName, params)
override fun receiveEvent(
surfaceId: Int,
targetTag: Int,
eventName: String,
params: WritableMap?,
) =
// We assume this event can't be coalesced. `customCoalesceKey` has no meaning in Fabric.
receiveEvent(surfaceId, targetTag, eventName, false, 0, params, EventCategoryDef.UNSPECIFIED)
@Deprecated("Deprecated in Java")
override fun receiveTouches(
eventName: String,
touches: WritableArray,
changedIndices: WritableArray,
) {
throw UnsupportedOperationException("receiveTouches is not support by native animated events")
}
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,280 @@
/*
* 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.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 {
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 {
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::class.java.name}")
)
}
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::class.java.name}")
)
}
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::class.java.name}")
)
}
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::class.java.name}"
)
}
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::class.java.name}")
)
}
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
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)
}
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()
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
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 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))
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,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.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 {
animationConfig.putNull("toValue")
}
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,45 @@
/*
* 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.common.annotations.internal.LegacyArchitecture
import com.facebook.react.common.annotations.internal.LegacyArchitectureLogLevel
import com.facebook.react.common.annotations.internal.LegacyArchitectureLogger
/** Implementation of javascript callback function that uses Bridge to schedule method execution. */
@LegacyArchitecture(logLevel = LegacyArchitectureLogLevel.ERROR)
@Deprecated(
message = "This class is part of Legacy Architecture and will be removed in a future release",
level = DeprecationLevel.WARNING,
)
internal class CallbackImpl(private val jsInstance: JSInstance, private val callbackId: Int) :
Callback {
private var invoked = false
override fun invoke(vararg args: Any?) {
if (invoked) {
throw RuntimeException(
"Illegal callback invocation from native module. This callback type only permits a single invocation from native code."
)
}
@Suppress("UNCHECKED_CAST")
jsInstance.invokeCallback(callbackId, Arguments.fromJavaArgs(args as Array<Any?>))
invoked = true
}
private companion object {
init {
LegacyArchitectureLogger.assertLegacyArchitecture(
"CallbackImpl",
LegacyArchitectureLogLevel.ERROR,
)
}
}
}

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.
*/
@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)
/**
* Adds a idle listener for this Catalyst instance. The listener will receive notifications
* whenever the bridge transitions from idle to busy and vice-versa, where the busy state is
* defined as there being some non-zero number of calls to JS that haven't resolved via a
* onBatchCompleted call. The listener should be purely passive and not affect application logic.
*/
public fun addBridgeIdleDebugListener(
@Suppress("DEPRECATION") listener: NotThreadSafeBridgeIdleDebugListener
)
/**
* Removes a NotThreadSafeBridgeIdleDebugListener previously added with
* [addBridgeIdleDebugListener]
*/
public fun removeBridgeIdleDebugListener(
@Suppress("DEPRECATION") listener: NotThreadSafeBridgeIdleDebugListener
)
/** 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,710 @@
/*
* 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.Assertions.assertCondition;
import static com.facebook.infer.annotation.ThreadConfined.UI;
import static com.facebook.systrace.Systrace.TRACE_TAG_REACT;
import android.content.res.AssetManager;
import androidx.annotation.Nullable;
import com.facebook.common.logging.FLog;
import com.facebook.infer.annotation.Assertions;
import com.facebook.infer.annotation.ThreadConfined;
import com.facebook.jni.HybridData;
import com.facebook.proguard.annotations.DoNotStrip;
import com.facebook.proguard.annotations.DoNotStripAny;
import com.facebook.react.bridge.queue.MessageQueueThread;
import com.facebook.react.bridge.queue.QueueThreadExceptionHandler;
import com.facebook.react.bridge.queue.ReactQueueConfiguration;
import com.facebook.react.bridge.queue.ReactQueueConfigurationImpl;
import com.facebook.react.bridge.queue.ReactQueueConfigurationSpec;
import com.facebook.react.common.ReactConstants;
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.internal.featureflags.ReactNativeNewArchitectureFeatureFlags;
import com.facebook.react.internal.turbomodule.core.interfaces.TurboModuleRegistry;
import com.facebook.react.module.annotations.ReactModule;
import com.facebook.react.turbomodule.core.CallInvokerHolderImpl;
import com.facebook.react.turbomodule.core.NativeMethodCallInvokerHolderImpl;
import com.facebook.systrace.Systrace;
import com.facebook.systrace.TraceListener;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collection;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicInteger;
/**
* This provides an implementation of the public CatalystInstance instance. It is public because it
* is built by ReactInstanceManager which is in a different package.
*/
@DoNotStrip
@LegacyArchitecture
@Deprecated(
since = "This class is part of Legacy Architecture and will be removed in a future release")
public class CatalystInstanceImpl implements CatalystInstance {
static {
ReactNativeJNISoLoader.staticInit();
LegacyArchitectureLogger.assertLegacyArchitecture(
"CatalystInstanceImpl", LegacyArchitectureLogLevel.WARNING);
}
private static final AtomicInteger sNextInstanceIdForTrace = new AtomicInteger(1);
public static class PendingJSCall {
public String mModule;
public String mMethod;
public @Nullable NativeArray mArguments;
public PendingJSCall(String module, String method, @Nullable NativeArray arguments) {
mModule = module;
mMethod = method;
mArguments = arguments;
}
void call(CatalystInstanceImpl catalystInstance) {
NativeArray arguments = mArguments != null ? mArguments : new WritableNativeArray();
catalystInstance.jniCallJSFunction(mModule, mMethod, arguments);
}
public String toString() {
return mModule
+ "."
+ mMethod
+ "("
+ (mArguments == null ? "" : mArguments.toString())
+ ")";
}
}
// Access from any thread
private final ReactQueueConfigurationImpl mReactQueueConfiguration;
private final CopyOnWriteArrayList<NotThreadSafeBridgeIdleDebugListener> mBridgeIdleListeners;
private final AtomicInteger mPendingJSCalls = new AtomicInteger(0);
private final String mJsPendingCallsTitleForTrace =
"pending_js_calls_instance" + sNextInstanceIdForTrace.getAndIncrement();
private volatile boolean mDestroyed = false;
private final TraceListener mTraceListener;
private final JavaScriptModuleRegistry mJSModuleRegistry;
private final JSBundleLoader mJSBundleLoader;
private final ArrayList<PendingJSCall> mJSCallsPendingInit = new ArrayList<>();
private final Object mJSCallsPendingInitLock = new Object();
private final NativeModuleRegistry mNativeModuleRegistry;
private final JSExceptionHandler mJSExceptionHandler;
private final MessageQueueThread mNativeModulesQueueThread;
private boolean mInitialized = false;
private volatile boolean mAcceptCalls = false;
private boolean mJSBundleHasLoaded;
private @Nullable String mSourceURL;
private JavaScriptContextHolder mJavaScriptContextHolder;
private @Nullable TurboModuleRegistry mTurboModuleRegistry;
private @Nullable UIManager mFabricUIManager;
// C++ parts
private final HybridData mHybridData;
private static native HybridData initHybrid();
public native CallInvokerHolderImpl getJSCallInvokerHolder();
public native NativeMethodCallInvokerHolderImpl getNativeMethodCallInvokerHolder();
private @Nullable ReactInstanceManagerInspectorTarget mInspectorTarget;
private CatalystInstanceImpl(
final ReactQueueConfigurationSpec reactQueueConfigurationSpec,
final JavaScriptExecutor jsExecutor,
final NativeModuleRegistry nativeModuleRegistry,
final JSBundleLoader jsBundleLoader,
JSExceptionHandler jSExceptionHandler,
@Nullable ReactInstanceManagerInspectorTarget inspectorTarget) {
FLog.d(ReactConstants.TAG, "Initializing React Xplat Bridge.");
Systrace.beginSection(TRACE_TAG_REACT, "createCatalystInstanceImpl");
mHybridData = initHybrid();
mReactQueueConfiguration =
ReactQueueConfigurationImpl.create(
reactQueueConfigurationSpec, new NativeExceptionHandler());
mBridgeIdleListeners = new CopyOnWriteArrayList<>();
mNativeModuleRegistry = nativeModuleRegistry;
mJSModuleRegistry = new JavaScriptModuleRegistry();
mJSBundleLoader = jsBundleLoader;
mJSExceptionHandler = jSExceptionHandler;
mNativeModulesQueueThread = mReactQueueConfiguration.getNativeModulesQueueThread();
mTraceListener = new JSProfilerTraceListener(this);
mInspectorTarget = inspectorTarget;
Systrace.endSection(TRACE_TAG_REACT);
FLog.d(ReactConstants.TAG, "Initializing React Xplat Bridge before initializeBridge");
Systrace.beginSection(TRACE_TAG_REACT, "initializeCxxBridge");
initializeBridge(
new InstanceCallback(this),
jsExecutor,
mReactQueueConfiguration.getJSQueueThread(),
mNativeModulesQueueThread,
mNativeModuleRegistry.getJavaModules(this),
mNativeModuleRegistry.getCxxModules(),
mInspectorTarget);
FLog.d(ReactConstants.TAG, "Initializing React Xplat Bridge after initializeBridge");
Systrace.endSection(TRACE_TAG_REACT);
mJavaScriptContextHolder = new JavaScriptContextHolder(getJavaScriptContext());
}
@DoNotStripAny
private static class InstanceCallback {
// We do this so the callback doesn't keep the CatalystInstanceImpl alive.
// In this case, the callback is held in C++ code, so the GC can't see it
// and determine there's an inaccessible cycle.
private final WeakReference<CatalystInstanceImpl> mOuter;
InstanceCallback(CatalystInstanceImpl outer) {
mOuter = new WeakReference<>(outer);
}
public void onBatchComplete() {
CatalystInstanceImpl impl = mOuter.get();
if (impl != null) {
impl.mNativeModulesQueueThread.runOnQueue(
() -> {
impl.mNativeModuleRegistry.onBatchComplete();
});
}
}
public void incrementPendingJSCalls() {
CatalystInstanceImpl impl = mOuter.get();
if (impl != null) {
impl.incrementPendingJSCalls();
}
}
public void decrementPendingJSCalls() {
CatalystInstanceImpl impl = mOuter.get();
if (impl != null) {
impl.decrementPendingJSCalls();
}
}
}
/**
* This method and the native below permits a CatalystInstance to extend the known Native modules.
* This registry contains only the new modules to load. The registry {@code mNativeModuleRegistry}
* updates internally to contain all the new modules, and generates the new registry for
* extracting just the new collections.
*/
@Override
public void extendNativeModules(NativeModuleRegistry modules) {
// Extend the Java-visible registry of modules
mNativeModuleRegistry.registerModules(modules);
Collection<JavaModuleWrapper> javaModules = modules.getJavaModules(this);
Collection<ModuleHolder> cxxModules = modules.getCxxModules();
// Extend the Cxx-visible registry of modules wrapped in appropriate interfaces
jniExtendNativeModules(javaModules, cxxModules);
}
private native void jniExtendNativeModules(
Collection<JavaModuleWrapper> javaModules, Collection<ModuleHolder> cxxModules);
private native void initializeBridge(
InstanceCallback callback,
JavaScriptExecutor jsExecutor,
MessageQueueThread jsQueue,
MessageQueueThread moduleQueue,
Collection<JavaModuleWrapper> javaModules,
Collection<ModuleHolder> cxxModules,
@Nullable ReactInstanceManagerInspectorTarget inspectorTarget);
@Override
public void setSourceURLs(String deviceURL, String remoteURL) {
mSourceURL = deviceURL;
jniSetSourceURL(remoteURL);
}
@Override
public void registerSegment(int segmentId, String path) {
jniRegisterSegment(segmentId, path);
}
@Override
public void loadScriptFromAssets(
AssetManager assetManager, String assetURL, boolean loadSynchronously) {
mSourceURL = assetURL;
jniLoadScriptFromAssets(assetManager, assetURL, loadSynchronously);
}
@Override
public void loadScriptFromFile(String fileName, String sourceURL, boolean loadSynchronously) {
mSourceURL = sourceURL;
jniLoadScriptFromFile(fileName, sourceURL, loadSynchronously);
}
@Override
public void loadSplitBundleFromFile(String fileName, String sourceURL) {
jniLoadScriptFromFile(fileName, sourceURL, false);
}
private native void jniSetSourceURL(String sourceURL);
private native void jniRegisterSegment(int segmentId, String path);
private native void jniLoadScriptFromAssets(
AssetManager assetManager, String assetURL, boolean loadSynchronously);
private native void jniLoadScriptFromFile(
String fileName, String sourceURL, boolean loadSynchronously);
@Override
public void runJSBundle() {
FLog.d(ReactConstants.TAG, "CatalystInstanceImpl.runJSBundle()");
Assertions.assertCondition(!mJSBundleHasLoaded, "JS bundle was already loaded!");
// incrementPendingJSCalls();
mJSBundleLoader.loadScript(CatalystInstanceImpl.this);
synchronized (mJSCallsPendingInitLock) {
// Loading the bundle is queued on the JS thread, but may not have
// run yet. It's safe to set this here, though, since any work it
// gates will be queued on the JS thread behind the load.
mAcceptCalls = true;
for (PendingJSCall function : mJSCallsPendingInit) {
function.call(this);
}
mJSCallsPendingInit.clear();
mJSBundleHasLoaded = true;
}
// This is registered after JS starts since it makes a JS call
Systrace.registerListener(mTraceListener);
}
@Override
public boolean hasRunJSBundle() {
synchronized (mJSCallsPendingInitLock) {
return mJSBundleHasLoaded && mAcceptCalls;
}
}
@Override
public @Nullable String getSourceURL() {
return mSourceURL;
}
private native void jniCallJSFunction(String module, String method, NativeArray arguments);
@Override
public void callFunction(final String module, final String method, final NativeArray arguments) {
callFunction(new PendingJSCall(module, method, arguments));
}
public void callFunction(PendingJSCall function) {
if (mDestroyed) {
final String call = function.toString();
FLog.w(ReactConstants.TAG, "Calling JS function after bridge has been destroyed: " + call);
return;
}
if (!mAcceptCalls) {
// Most of the time the instance is initialized and we don't need to acquire the lock
synchronized (mJSCallsPendingInitLock) {
if (!mAcceptCalls) {
mJSCallsPendingInit.add(function);
return;
}
}
}
function.call(this);
}
private native void jniCallJSCallback(int callbackID, NativeArray arguments);
@Override
public void invokeCallback(final int callbackID, final NativeArrayInterface arguments) {
if (mDestroyed) {
FLog.w(ReactConstants.TAG, "Invoking JS callback after bridge has been destroyed.");
return;
}
jniCallJSCallback(callbackID, (NativeArray) arguments);
}
private native void unregisterFromInspector();
/**
* 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.
*/
@Override
@ThreadConfined(UI)
public void destroy() {
FLog.d(ReactConstants.TAG, "CatalystInstanceImpl.destroy() start");
UiThreadUtil.assertOnUiThread();
if (mDestroyed) {
return;
}
if (mInspectorTarget != null) {
assertCondition(
mInspectorTarget.isValid(),
"ReactInstanceManager inspector target destroyed before instance was unregistered");
}
unregisterFromInspector();
// TODO: tell all APIs to shut down
ReactMarker.logMarker(ReactMarkerConstants.DESTROY_CATALYST_INSTANCE_START);
mDestroyed = true;
mNativeModulesQueueThread.runOnQueue(
() -> {
mNativeModuleRegistry.notifyJSInstanceDestroy();
if (mFabricUIManager != null) {
mFabricUIManager.invalidate();
}
boolean wasIdle = (mPendingJSCalls.getAndSet(0) == 0);
if (!mBridgeIdleListeners.isEmpty()) {
for (NotThreadSafeBridgeIdleDebugListener listener : mBridgeIdleListeners) {
if (!wasIdle) {
listener.onTransitionToBridgeIdle();
}
listener.onBridgeDestroyed();
}
}
getReactQueueConfiguration()
.getJSQueueThread()
.runOnQueue(
() -> {
// We need to destroy the TurboModuleManager on the JS Thread
if (mTurboModuleRegistry != null) {
mTurboModuleRegistry.invalidate();
}
// Kill non-UI threads from neutral third party
// potentially expensive, so don't run on UI thread
new Thread(
() -> {
// contextHolder is used as a lock to guard against
// other users of the JS VM having the VM destroyed
// underneath them, so notify them before we reset
// Native
mJavaScriptContextHolder.clear();
mHybridData.resetNative();
getReactQueueConfiguration().destroy();
FLog.w(ReactConstants.TAG, "CatalystInstanceImpl.destroy() end");
ReactMarker.logMarker(
ReactMarkerConstants.DESTROY_CATALYST_INSTANCE_END);
},
"destroy_react_context")
.start();
});
});
// This is a noop if the listener was not yet registered.
Systrace.unregisterListener(mTraceListener);
}
@Override
public boolean isDestroyed() {
return mDestroyed;
}
/** Initialize all the native modules */
@VisibleForTesting
@Override
public void initialize() {
FLog.d(ReactConstants.TAG, "CatalystInstanceImpl.initialize()");
Assertions.assertCondition(
!mInitialized, "This catalyst instance has already been initialized");
// We assume that the instance manager blocks on running the JS bundle. If
// that changes, then we need to set mAcceptCalls just after posting the
// task that will run the js bundle.
Assertions.assertCondition(mAcceptCalls, "RunJSBundle hasn't completed.");
mInitialized = true;
mNativeModulesQueueThread.runOnQueue(
() -> {
mNativeModuleRegistry.notifyJSInstanceInitialized();
});
}
@Override
public ReactQueueConfiguration getReactQueueConfiguration() {
return mReactQueueConfiguration;
}
@Override
public <T extends JavaScriptModule> T getJSModule(Class<T> jsInterface) {
return mJSModuleRegistry.getJavaScriptModule(this, jsInterface);
}
@Override
public <T extends NativeModule> boolean hasNativeModule(Class<T> nativeModuleInterface) {
String moduleName = getNameFromAnnotation(nativeModuleInterface);
return getTurboModuleRegistry() != null && getTurboModuleRegistry().hasModule(moduleName)
? true
: mNativeModuleRegistry.hasModule(moduleName);
}
@Override
@Nullable
public <T extends NativeModule> T getNativeModule(Class<T> nativeModuleInterface) {
return (T) getNativeModule(getNameFromAnnotation(nativeModuleInterface));
}
private TurboModuleRegistry getTurboModuleRegistry() {
if (ReactNativeNewArchitectureFeatureFlags.useTurboModules()) {
return Assertions.assertNotNull(
mTurboModuleRegistry,
"TurboModules are enabled, but mTurboModuleRegistry hasn't been set.");
}
return null;
}
@Override
@Nullable
public NativeModule getNativeModule(String moduleName) {
if (getTurboModuleRegistry() != null) {
NativeModule module = getTurboModuleRegistry().getModule(moduleName);
if (module != null) {
return module;
}
}
return mNativeModuleRegistry.hasModule(moduleName)
? mNativeModuleRegistry.getModule(moduleName)
: null;
}
private <T extends NativeModule> String getNameFromAnnotation(Class<T> nativeModuleInterface) {
ReactModule annotation = nativeModuleInterface.getAnnotation(ReactModule.class);
if (annotation == null) {
throw new IllegalArgumentException(
"Could not find @ReactModule annotation in " + nativeModuleInterface.getCanonicalName());
}
return annotation.name();
}
// This is only used by com.facebook.react.modules.common.ModuleDataCleaner
@Override
public Collection<NativeModule> getNativeModules() {
Collection<NativeModule> nativeModules = new ArrayList<>();
nativeModules.addAll(mNativeModuleRegistry.getAllModules());
if (getTurboModuleRegistry() != null) {
for (NativeModule module : getTurboModuleRegistry().getModules()) {
nativeModules.add(module);
}
}
return nativeModules;
}
private native void jniHandleMemoryPressure(int level);
@Override
public void handleMemoryPressure(int level) {
if (mDestroyed) {
return;
}
jniHandleMemoryPressure(level);
}
/**
* Adds a idle listener for this Catalyst instance. The listener will receive notifications
* whenever the bridge transitions from idle to busy and vice-versa, where the busy state is
* defined as there being some non-zero number of calls to JS that haven't resolved via a
* onBatchComplete call. The listener should be purely passive and not affect application logic.
*
* @noinspection deprecation
*/
@Override
public void addBridgeIdleDebugListener(NotThreadSafeBridgeIdleDebugListener listener) {
mBridgeIdleListeners.add(listener);
}
/**
* Removes a NotThreadSafeBridgeIdleDebugListener previously added with {@link
* #addBridgeIdleDebugListener}
*
* @noinspection deprecation
*/
@Override
public void removeBridgeIdleDebugListener(NotThreadSafeBridgeIdleDebugListener listener) {
mBridgeIdleListeners.remove(listener);
}
@Override
public native void setGlobalVariable(String propName, String jsonValue);
@Override
public JavaScriptContextHolder getJavaScriptContextHolder() {
return mJavaScriptContextHolder;
}
public native RuntimeExecutor getRuntimeExecutor();
public native RuntimeScheduler getRuntimeScheduler();
private native long getJavaScriptContext();
private void incrementPendingJSCalls() {
int oldPendingCalls = mPendingJSCalls.getAndIncrement();
boolean wasIdle = oldPendingCalls == 0;
Systrace.traceCounter(TRACE_TAG_REACT, mJsPendingCallsTitleForTrace, oldPendingCalls + 1);
if (wasIdle && !mBridgeIdleListeners.isEmpty()) {
mNativeModulesQueueThread.runOnQueue(
() -> {
for (NotThreadSafeBridgeIdleDebugListener listener : mBridgeIdleListeners) {
listener.onTransitionToBridgeBusy();
}
});
}
}
@Override
public void setTurboModuleRegistry(TurboModuleRegistry turboModuleRegistry) {
mTurboModuleRegistry = turboModuleRegistry;
}
@Override
public void setFabricUIManager(UIManager fabricUIManager) {
mFabricUIManager = fabricUIManager;
}
@Override
public UIManager getFabricUIManager() {
return mFabricUIManager;
}
private void decrementPendingJSCalls() {
int newPendingCalls = mPendingJSCalls.decrementAndGet();
// TODO(9604406): handle case of web workers injecting messages to main thread
// Assertions.assertCondition(newPendingCalls >= 0);
boolean isNowIdle = newPendingCalls == 0;
Systrace.traceCounter(TRACE_TAG_REACT, mJsPendingCallsTitleForTrace, newPendingCalls);
if (isNowIdle && !mBridgeIdleListeners.isEmpty()) {
mNativeModulesQueueThread.runOnQueue(
() -> {
for (NotThreadSafeBridgeIdleDebugListener listener : mBridgeIdleListeners) {
listener.onTransitionToBridgeIdle();
}
});
}
}
private void onNativeException(Exception e) {
mJSExceptionHandler.handleException(e);
mReactQueueConfiguration
.getUIQueueThread()
.runOnQueue(
() -> {
destroy();
});
}
private class NativeExceptionHandler implements QueueThreadExceptionHandler {
@Override
public void handleException(Exception e) {
// Any Exception caught here is because of something in JS. Even if it's a bug in the
// framework/native code, it was triggered by JS and theoretically since we were able
// to set up the bridge, JS could change its logic, reload, and not trigger that crash.
onNativeException(e);
}
}
private static class JSProfilerTraceListener implements TraceListener {
// We do this so the callback doesn't keep the CatalystInstanceImpl alive.
// In this case, Systrace will keep the registered listener around forever
// if the CatalystInstanceImpl is not explicitly destroyed. These instances
// can still leak, but they are at least small.
private final WeakReference<CatalystInstanceImpl> mOuter;
public JSProfilerTraceListener(CatalystInstanceImpl outer) {
mOuter = new WeakReference<CatalystInstanceImpl>(outer);
}
@Override
public void onTraceStarted() {
CatalystInstanceImpl impl = mOuter.get();
if (impl != null) {
impl.getJSModule(com.facebook.react.bridge.Systrace.class).setEnabled(true);
}
}
@Override
public void onTraceStopped() {
CatalystInstanceImpl impl = mOuter.get();
if (impl != null) {
impl.getJSModule(com.facebook.react.bridge.Systrace.class).setEnabled(false);
}
}
}
public static class Builder {
private @Nullable ReactQueueConfigurationSpec mReactQueueConfigurationSpec;
private @Nullable JSBundleLoader mJSBundleLoader;
private @Nullable NativeModuleRegistry mRegistry;
private @Nullable JavaScriptExecutor mJSExecutor;
private @Nullable JSExceptionHandler mJSExceptionHandler;
private @Nullable ReactInstanceManagerInspectorTarget mInspectorTarget;
public Builder setReactQueueConfigurationSpec(
ReactQueueConfigurationSpec ReactQueueConfigurationSpec) {
mReactQueueConfigurationSpec = ReactQueueConfigurationSpec;
return this;
}
public Builder setRegistry(NativeModuleRegistry registry) {
mRegistry = registry;
return this;
}
public Builder setJSBundleLoader(JSBundleLoader jsBundleLoader) {
mJSBundleLoader = jsBundleLoader;
return this;
}
public Builder setJSExecutor(JavaScriptExecutor jsExecutor) {
mJSExecutor = jsExecutor;
return this;
}
public Builder setJSExceptionHandler(JSExceptionHandler handler) {
mJSExceptionHandler = handler;
return this;
}
public Builder setInspectorTarget(
@Nullable ReactInstanceManagerInspectorTarget inspectorTarget) {
mInspectorTarget = inspectorTarget;
return this;
}
public CatalystInstanceImpl build() {
return new CatalystInstanceImpl(
Assertions.assertNotNull(mReactQueueConfigurationSpec),
Assertions.assertNotNull(mJSExecutor),
Assertions.assertNotNull(mRegistry),
Assertions.assertNotNull(mJSBundleLoader),
Assertions.assertNotNull(mJSExceptionHandler),
mInspectorTarget);
}
}
}

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,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.HybridData
import com.facebook.proguard.annotations.DoNotStrip
import com.facebook.react.common.annotations.internal.InteropLegacyArchitecture
import com.facebook.react.common.annotations.internal.LegacyArchitectureLogger
/** This does nothing interesting, except avoid breaking existing code. */
@DoNotStrip
@InteropLegacyArchitecture
public open class CxxModuleWrapper protected constructor(hybridData: HybridData) :
CxxModuleWrapperBase(hybridData) {
private companion object {
init {
LegacyArchitectureLogger.assertLegacyArchitecture("CxxModuleWrapper")
}
}
}

View File

@@ -0,0 +1,61 @@
/*
* 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.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
/**
* A Java Object which represents a cross-platform C++ module
*
* This module implements the [NativeModule] interface but will never be invoked from Java, instead
* the underlying Cxx module will be extracted by the bridge and called directly.
*/
@DoNotStrip
@InteropLegacyArchitecture
public open class CxxModuleWrapperBase
protected constructor(
// For creating a wrapper from C++, or from a derived class.
@Suppress("NoHungarianNotation") @DoNotStrip private var mHybridData: HybridData
) : NativeModule {
external override fun getName(): String
override fun initialize() {
// do nothing
}
@Deprecated(
"The method canOverrideExistingModule is not used in the New Architecture and will be removed in a future release."
)
override fun canOverrideExistingModule(): Boolean = false
override fun invalidate() {
mHybridData.resetNative()
}
// Replace the current native module held by this wrapper by a new instance
protected fun resetModule(hd: HybridData) {
if (hd !== mHybridData) {
mHybridData.resetNative()
mHybridData = hd
}
}
private companion object {
init {
ReactNativeJNISoLoader.staticInit()
LegacyArchitectureLogger.assertLegacyArchitecture(
"CxxModuleWrapperBase",
LegacyArchitectureLogLevel.WARNING,
)
}
}
}

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,61 @@
/*
* 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
/** 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.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 = Pools.SimplePool<DynamicFromArray>(10)
@JvmStatic
fun create(array: ReadableArray, index: Int): DynamicFromArray {
val dynamic = pool.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)
}

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