diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index 97ee3b9263a..c3c38799bb1 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -1032,8 +1032,7 @@ public abstract interface class com/facebook/react/bridge/Promise { public abstract fun resolve (Ljava/lang/Object;)V } -public final class com/facebook/react/bridge/PromiseImpl : com/facebook/react/bridge/Promise { - public static final field Companion Lcom/facebook/react/bridge/PromiseImpl$Companion; +public class com/facebook/react/bridge/PromiseImpl : com/facebook/react/bridge/Promise { public fun (Lcom/facebook/react/bridge/Callback;Lcom/facebook/react/bridge/Callback;)V public fun reject (Ljava/lang/String;)V public fun reject (Ljava/lang/String;Lcom/facebook/react/bridge/WritableMap;)V @@ -1048,9 +1047,6 @@ public final class com/facebook/react/bridge/PromiseImpl : com/facebook/react/br public fun resolve (Ljava/lang/Object;)V } -public final class com/facebook/react/bridge/PromiseImpl$Companion { -} - public abstract class com/facebook/react/bridge/ReactApplicationContext : com/facebook/react/bridge/ReactContext { public fun (Landroid/content/Context;)V } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/PromiseImpl.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/PromiseImpl.java new file mode 100644 index 00000000000..97340676fd2 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/PromiseImpl.java @@ -0,0 +1,257 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.bridge; + +import androidx.annotation.Nullable; +import com.facebook.infer.annotation.Nullsafe; +import com.facebook.proguard.annotations.DoNotStrip; + +/* + * Implementation of {@link Promise} that represents a JavaScript Promise which can be passed to the + * native module as a method parameter. + * + * Methods annotated with {@link ReactMethod} that use a {@link Promise} as the last parameter + * will be marked as "promise" and will return a promise when invoked from JavaScript. + */ +@Nullsafe(Nullsafe.Mode.LOCAL) +@DoNotStrip +public class PromiseImpl implements Promise { + // Number of stack frames to parse and return to mReject.invoke + // for ERROR_MAP_KEY_NATIVE_STACK + private static final int ERROR_STACK_FRAME_LIMIT = 50; + + private static final String ERROR_DEFAULT_CODE = "EUNSPECIFIED"; + private static final String ERROR_DEFAULT_MESSAGE = "Error not specified."; + + // Keys for mReject's WritableMap + private static final String ERROR_MAP_KEY_CODE = "code"; + private static final String ERROR_MAP_KEY_MESSAGE = "message"; + private static final String ERROR_MAP_KEY_NAME = "name"; + private static final String ERROR_MAP_KEY_USER_INFO = "userInfo"; + private static final String ERROR_MAP_KEY_NATIVE_STACK = "nativeStackAndroid"; + + // Keys for ERROR_MAP_KEY_NATIVE_STACK's StackFrame maps + private static final String STACK_FRAME_KEY_CLASS = "class"; + private static final String STACK_FRAME_KEY_FILE = "file"; + private static final String STACK_FRAME_KEY_LINE_NUMBER = "lineNumber"; + private static final String STACK_FRAME_KEY_METHOD_NAME = "methodName"; + + private @Nullable Callback mResolve; + private @Nullable Callback mReject; + + @DoNotStrip + public PromiseImpl(@Nullable Callback resolve, @Nullable Callback reject) { + mResolve = resolve; + mReject = reject; + } + + /** + * Successfully resolve the Promise with an optional value. + * + * @param value Object + */ + @Override + public void resolve(@Nullable Object value) { + if (mResolve != null) { + mResolve.invoke(value); + mResolve = null; + mReject = null; + } + } + + /** + * Report an error without an exception using a custom code and error message. + * + * @param code String + * @param message String + */ + @Override + public void reject(String code, @Nullable String message) { + reject(code, message, /*Throwable*/ null, /*WritableMap*/ null); + } + + /** + * Report an exception with a custom code. + * + * @param code String + * @param throwable Throwable + */ + @Override + public void reject(String code, @Nullable Throwable throwable) { + reject(code, /*Message*/ null, throwable, /*WritableMap*/ null); + } + + /** + * Report an exception with a custom code and error message. + * + * @param code String + * @param message String + * @param throwable Throwable + */ + @Override + public void reject(String code, @Nullable String message, @Nullable Throwable throwable) { + reject(code, message, throwable, /*WritableMap*/ null); + } + + /** + * Report an exception, with default error code. Useful in catch-all scenarios where it's unclear + * why the error occurred. + * + * @param throwable Throwable + */ + @Override + public void reject(Throwable throwable) { + reject(/*Code*/ null, /*Message*/ null, throwable, /*WritableMap*/ null); + } + + /* --------------------------- + * With userInfo WritableMap + * --------------------------- */ + + /** + * Report an exception, with default error code, with userInfo. Useful in catch-all scenarios + * where it's unclear why the error occurred. + * + * @param throwable Throwable + * @param userInfo WritableMap + */ + @Override + public void reject(Throwable throwable, WritableMap userInfo) { + reject(/*Code*/ null, /*Message*/ null, throwable, userInfo); + } + + /** + * Reject with a code and userInfo WritableMap. + * + * @param code String + * @param userInfo WritableMap + */ + @Override + public void reject(String code, WritableMap userInfo) { + reject(code, /*Message*/ null, /*Throwable*/ null, userInfo); + } + + /** + * Report an exception with a custom code and userInfo. + * + * @param code String + * @param throwable Throwable + * @param userInfo WritableMap + */ + @Override + public void reject(String code, @Nullable Throwable throwable, WritableMap userInfo) { + reject(code, /*Message*/ null, throwable, userInfo); + } + + /** + * Report an error with a custom code, error message and userInfo, an error not caused by an + * exception. + * + * @param code String + * @param message String + * @param userInfo WritableMap + */ + @Override + public void reject(String code, @Nullable String message, WritableMap userInfo) { + reject(code, message, /*Throwable*/ null, userInfo); + } + + /** + * Report an exception with a custom code, error message and userInfo. + * + * @param code String + * @param message String + * @param throwable Throwable + * @param userInfo WritableMap + */ + @Override + public void reject( + @Nullable String code, + @Nullable String message, + @Nullable Throwable throwable, + @Nullable WritableMap userInfo) { + if (mReject == null) { + mResolve = null; + return; + } + + WritableNativeMap errorInfo = new WritableNativeMap(); + + if (code == null) { + errorInfo.putString(ERROR_MAP_KEY_CODE, ERROR_DEFAULT_CODE); + } else { + errorInfo.putString(ERROR_MAP_KEY_CODE, code); + } + + // Use the custom message if provided otherwise use the throwable message. + if (message != null) { + errorInfo.putString(ERROR_MAP_KEY_MESSAGE, message); + } else if (throwable != null) { + String throwableMessage = throwable.getMessage(); + // Fallback to the trowable name, so we record some useful information + if (throwableMessage == null || throwableMessage.isEmpty()) { + throwableMessage = throwable.getClass().getCanonicalName(); + } + errorInfo.putString(ERROR_MAP_KEY_MESSAGE, throwableMessage); + } else { + // The JavaScript side expects a map with at least an error message. + // /Libraries/BatchedBridge/NativeModules.js -> createErrorFromErrorData + // TYPE: (errorData: { message: string }) + errorInfo.putString(ERROR_MAP_KEY_MESSAGE, ERROR_DEFAULT_MESSAGE); + } + + // For consistency with iOS ensure userInfo key exists, even if we null it. + // iOS: /React/Base/RCTUtils.m -> RCTJSErrorFromCodeMessageAndNSError + if (userInfo != null) { + errorInfo.putMap(ERROR_MAP_KEY_USER_INFO, userInfo); + } else { + errorInfo.putNull(ERROR_MAP_KEY_USER_INFO); + } + + // Attach a nativeStackAndroid array if a throwable was passed + // this matches iOS behavior - iOS adds a `nativeStackIOS` property + // iOS: /React/Base/RCTUtils.m -> RCTJSErrorFromCodeMessageAndNSError + if (throwable != null) { + errorInfo.putString(ERROR_MAP_KEY_NAME, throwable.getClass().getCanonicalName()); + + StackTraceElement[] stackTrace = throwable.getStackTrace(); + WritableNativeArray nativeStackAndroid = new WritableNativeArray(); + + // Build an an Array of StackFrames to match JavaScript: + // iOS: /Libraries/Core/Devtools/parseErrorStack.js -> StackFrame + for (int i = 0; i < stackTrace.length && i < ERROR_STACK_FRAME_LIMIT; i++) { + StackTraceElement frame = stackTrace[i]; + WritableMap frameMap = new WritableNativeMap(); + // NOTE: no column number exists StackTraceElement + frameMap.putString(STACK_FRAME_KEY_CLASS, frame.getClassName()); + frameMap.putString(STACK_FRAME_KEY_FILE, frame.getFileName()); + frameMap.putInt(STACK_FRAME_KEY_LINE_NUMBER, frame.getLineNumber()); + frameMap.putString(STACK_FRAME_KEY_METHOD_NAME, frame.getMethodName()); + nativeStackAndroid.pushMap(frameMap); + } + + errorInfo.putArray(ERROR_MAP_KEY_NATIVE_STACK, nativeStackAndroid); + } else { + errorInfo.putArray(ERROR_MAP_KEY_NATIVE_STACK, new WritableNativeArray()); + } + + mReject.invoke(errorInfo); + mResolve = null; + mReject = null; + } + + /* ------------ + * Deprecated + * ------------ */ + + @Override + @Deprecated + public void reject(String message) { + reject(/*Code*/ null, message, /*Throwable*/ null, /*WritableMap*/ null); + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/PromiseImpl.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/PromiseImpl.kt deleted file mode 100644 index f0070dd70c7..00000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/PromiseImpl.kt +++ /dev/null @@ -1,238 +0,0 @@ -/* - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -package com.facebook.react.bridge - -import com.facebook.proguard.annotations.DoNotStrip - -/** - * Implementation of [Promise] that represents a JavaScript Promise which can be passed to the - * native module as a method parameter. Methods annotated with [ReactMethod] that use a [Promise] as - * the last parameter will be marked as "promise" and will return a promise when invoked from - * JavaScript. - */ -@DoNotStrip -public class PromiseImpl(private var resolve: Callback?, private var reject: Callback?) : Promise { - /** - * Successfully resolve the [Promise] with an optional value. - * - * @param value Object - */ - override fun resolve(value: Any?) { - resolve?.let { res -> - res.invoke(value) - resolve = null - reject = null - } - } - - /** - * Report an error without an exception using a custom code and error message. - * - * @param code String - * @param message String - */ - override fun reject(code: String, message: String?) { - reject(code, message, null, null) - } - - /** - * Report an exception with a custom code. - * - * @param code String - * @param throwable Throwable - */ - override fun reject(code: String, throwable: Throwable?) { - reject(code, null, throwable, null) - } - - /** - * Report an exception with a custom code and error message. - * - * @param code String - * @param message String - * @param throwable Throwable - */ - override fun reject(code: String, message: String?, throwable: Throwable?) { - reject(code, message, throwable, null) - } - - /** - * Report an exception, with default error code. Useful in catch-all scenarios where it's unclear - * why the error occurred. - * - * @param throwable Throwable - */ - override fun reject(throwable: Throwable) { - reject(null, null, throwable, null) - } - - /* --------------------------- - * With userInfo WritableMap - * --------------------------- */ - - /** - * Report an exception, with default error code, with userInfo. Useful in catch-all scenarios - * where it's unclear why the error occurred. - * - * @param throwable Throwable - * @param userInfo WritableMap - */ - override fun reject(throwable: Throwable, userInfo: WritableMap) { - reject(null, null, throwable, userInfo) - } - - /** - * Reject with a code and userInfo WritableMap. - * - * @param code String - * @param userInfo WritableMap - */ - override fun reject(code: String, userInfo: WritableMap) { - reject(code, null, null, userInfo) - } - - /** - * Report an exception with a custom code and userInfo. - * - * @param code String - * @param throwable Throwable - * @param userInfo WritableMap - */ - override fun reject(code: String, throwable: Throwable?, userInfo: WritableMap) { - reject(code, null, throwable, userInfo) - } - - /** - * Report an error with a custom code, error message and userInfo, an error not caused by an - * exception. - * - * @param code String - * @param message String - * @param userInfo WritableMap - */ - override fun reject(code: String, message: String?, userInfo: WritableMap) { - reject(code, message, null, userInfo) - } - - /** - * Report an exception with a custom code, error message and userInfo. - * - * @param code String - * @param message String - * @param throwable Throwable - * @param userInfo WritableMap - */ - override fun reject( - code: String?, - message: String?, - throwable: Throwable?, - userInfo: WritableMap? - ) { - if (reject == null) { - resolve = null - return - } - - val errorInfo = WritableNativeMap() - - if (code == null) { - errorInfo.putString(ERROR_MAP_KEY_CODE, ERROR_DEFAULT_CODE) - } else { - errorInfo.putString(ERROR_MAP_KEY_CODE, code) - } - - // Use the custom message if provided otherwise use the throwable message. - if (message != null) { - errorInfo.putString(ERROR_MAP_KEY_MESSAGE, message) - } else if (throwable != null) { - var throwableMessage = throwable.message - // Fallback to the throwable name, so we record some useful information - if (throwableMessage.isNullOrEmpty()) { - throwableMessage = throwable.javaClass.canonicalName - } - errorInfo.putString(ERROR_MAP_KEY_MESSAGE, throwableMessage) - } else { - // The JavaScript side expects a map with at least an error message. - // /Libraries/BatchedBridge/NativeModules.js -> createErrorFromErrorData - // TYPE: (errorData: { message: string }) - errorInfo.putString(ERROR_MAP_KEY_MESSAGE, ERROR_DEFAULT_MESSAGE) - } - - // For consistency with iOS ensure userInfo key exists, even if we null it. - // iOS: /React/Base/RCTUtils.m -> RCTJSErrorFromCodeMessageAndNSError - if (userInfo != null) { - errorInfo.putMap(ERROR_MAP_KEY_USER_INFO, userInfo) - } else { - errorInfo.putNull(ERROR_MAP_KEY_USER_INFO) - } - - // Attach a nativeStackAndroid array if a throwable was passed - // this matches iOS behavior - iOS adds a `nativeStackIOS` property - // iOS: /React/Base/RCTUtils.m -> RCTJSErrorFromCodeMessageAndNSError - if (throwable != null) { - errorInfo.putString(ERROR_MAP_KEY_NAME, throwable.javaClass.canonicalName) - - val stackTrace = throwable.stackTrace - val nativeStackAndroid = WritableNativeArray() - - // Build an an Array of StackFrames to match JavaScript: - // iOS: /Libraries/Core/Devtools/parseErrorStack.js -> StackFrame - var i = 0 - while (i < stackTrace.size && i < ERROR_STACK_FRAME_LIMIT) { - val frame = stackTrace[i] - val frameMap: WritableMap = WritableNativeMap() - // NOTE: no column number exists StackTraceElement - frameMap.putString(STACK_FRAME_KEY_CLASS, frame.className) - frameMap.putString(STACK_FRAME_KEY_FILE, frame.fileName) - frameMap.putInt(STACK_FRAME_KEY_LINE_NUMBER, frame.lineNumber) - frameMap.putString(STACK_FRAME_KEY_METHOD_NAME, frame.methodName) - nativeStackAndroid.pushMap(frameMap) - i++ - } - - errorInfo.putArray(ERROR_MAP_KEY_NATIVE_STACK, nativeStackAndroid) - } else { - errorInfo.putArray(ERROR_MAP_KEY_NATIVE_STACK, WritableNativeArray()) - } - - reject?.invoke(errorInfo) - resolve = null - reject = null - } - - /* ------------ - * Deprecated - * ------------ */ - - @Deprecated("Use reject(code, message) instead.", ReplaceWith("reject(code, message)")) - override fun reject(message: String) { - reject(null, message, null, null) - } - - public companion object { - // Number of stack frames to parse and return to reject.invoke - // for ERROR_MAP_KEY_NATIVE_STACK - private const val ERROR_STACK_FRAME_LIMIT = 50 - - private const val ERROR_DEFAULT_CODE = "EUNSPECIFIED" - private const val ERROR_DEFAULT_MESSAGE = "Error not specified." - - // Keys for reject's WritableMap - private const val ERROR_MAP_KEY_CODE = "code" - private const val ERROR_MAP_KEY_MESSAGE = "message" - private const val ERROR_MAP_KEY_NAME = "name" - private const val ERROR_MAP_KEY_USER_INFO = "userInfo" - private const val ERROR_MAP_KEY_NATIVE_STACK = "nativeStackAndroid" - - // Keys for ERROR_MAP_KEY_NATIVE_STACK's StackFrame maps - private const val STACK_FRAME_KEY_CLASS = "class" - private const val STACK_FRAME_KEY_FILE = "file" - private const val STACK_FRAME_KEY_LINE_NUMBER = "lineNumber" - private const val STACK_FRAME_KEY_METHOD_NAME = "methodName" - } -}