|
| 1 | +/* |
| 2 | + * Copyright (c) Meta Platforms, Inc. and affiliates. |
| 3 | + * |
| 4 | + * This source code is licensed under the MIT license found in the |
| 5 | + * LICENSE file in the root directory of this source tree. |
| 6 | + */ |
| 7 | + |
| 8 | +package com.facebook.react.bridge; |
| 9 | + |
| 10 | +import androidx.annotation.Nullable; |
| 11 | +import com.facebook.infer.annotation.Nullsafe; |
| 12 | +import com.facebook.proguard.annotations.DoNotStrip; |
| 13 | + |
| 14 | +/* |
| 15 | + * Implementation of {@link Promise} that represents a JavaScript Promise which can be passed to the |
| 16 | + * native module as a method parameter. |
| 17 | + * |
| 18 | + * Methods annotated with {@link ReactMethod} that use a {@link Promise} as the last parameter |
| 19 | + * will be marked as "promise" and will return a promise when invoked from JavaScript. |
| 20 | + */ |
| 21 | +@Nullsafe(Nullsafe.Mode.LOCAL) |
| 22 | +@DoNotStrip |
| 23 | +public class PromiseImpl implements Promise { |
| 24 | + // Number of stack frames to parse and return to mReject.invoke |
| 25 | + // for ERROR_MAP_KEY_NATIVE_STACK |
| 26 | + private static final int ERROR_STACK_FRAME_LIMIT = 50; |
| 27 | + |
| 28 | + private static final String ERROR_DEFAULT_CODE = "EUNSPECIFIED"; |
| 29 | + private static final String ERROR_DEFAULT_MESSAGE = "Error not specified."; |
| 30 | + |
| 31 | + // Keys for mReject's WritableMap |
| 32 | + private static final String ERROR_MAP_KEY_CODE = "code"; |
| 33 | + private static final String ERROR_MAP_KEY_MESSAGE = "message"; |
| 34 | + private static final String ERROR_MAP_KEY_NAME = "name"; |
| 35 | + private static final String ERROR_MAP_KEY_USER_INFO = "userInfo"; |
| 36 | + private static final String ERROR_MAP_KEY_NATIVE_STACK = "nativeStackAndroid"; |
| 37 | + |
| 38 | + // Keys for ERROR_MAP_KEY_NATIVE_STACK's StackFrame maps |
| 39 | + private static final String STACK_FRAME_KEY_CLASS = "class"; |
| 40 | + private static final String STACK_FRAME_KEY_FILE = "file"; |
| 41 | + private static final String STACK_FRAME_KEY_LINE_NUMBER = "lineNumber"; |
| 42 | + private static final String STACK_FRAME_KEY_METHOD_NAME = "methodName"; |
| 43 | + |
| 44 | + private @Nullable Callback mResolve; |
| 45 | + private @Nullable Callback mReject; |
| 46 | + |
| 47 | + @DoNotStrip |
| 48 | + public PromiseImpl(@Nullable Callback resolve, @Nullable Callback reject) { |
| 49 | + mResolve = resolve; |
| 50 | + mReject = reject; |
| 51 | + } |
| 52 | + |
| 53 | + /** |
| 54 | + * Successfully resolve the Promise with an optional value. |
| 55 | + * |
| 56 | + * @param value Object |
| 57 | + */ |
| 58 | + @Override |
| 59 | + public void resolve(@Nullable Object value) { |
| 60 | + if (mResolve != null) { |
| 61 | + mResolve.invoke(value); |
| 62 | + mResolve = null; |
| 63 | + mReject = null; |
| 64 | + } |
| 65 | + } |
| 66 | + |
| 67 | + /** |
| 68 | + * Report an error without an exception using a custom code and error message. |
| 69 | + * |
| 70 | + * @param code String |
| 71 | + * @param message String |
| 72 | + */ |
| 73 | + @Override |
| 74 | + public void reject(String code, @Nullable String message) { |
| 75 | + reject(code, message, /*Throwable*/ null, /*WritableMap*/ null); |
| 76 | + } |
| 77 | + |
| 78 | + /** |
| 79 | + * Report an exception with a custom code. |
| 80 | + * |
| 81 | + * @param code String |
| 82 | + * @param throwable Throwable |
| 83 | + */ |
| 84 | + @Override |
| 85 | + public void reject(String code, @Nullable Throwable throwable) { |
| 86 | + reject(code, /*Message*/ null, throwable, /*WritableMap*/ null); |
| 87 | + } |
| 88 | + |
| 89 | + /** |
| 90 | + * Report an exception with a custom code and error message. |
| 91 | + * |
| 92 | + * @param code String |
| 93 | + * @param message String |
| 94 | + * @param throwable Throwable |
| 95 | + */ |
| 96 | + @Override |
| 97 | + public void reject(String code, @Nullable String message, @Nullable Throwable throwable) { |
| 98 | + reject(code, message, throwable, /*WritableMap*/ null); |
| 99 | + } |
| 100 | + |
| 101 | + /** |
| 102 | + * Report an exception, with default error code. Useful in catch-all scenarios where it's unclear |
| 103 | + * why the error occurred. |
| 104 | + * |
| 105 | + * @param throwable Throwable |
| 106 | + */ |
| 107 | + @Override |
| 108 | + public void reject(Throwable throwable) { |
| 109 | + reject(/*Code*/ null, /*Message*/ null, throwable, /*WritableMap*/ null); |
| 110 | + } |
| 111 | + |
| 112 | + /* --------------------------- |
| 113 | + * With userInfo WritableMap |
| 114 | + * --------------------------- */ |
| 115 | + |
| 116 | + /** |
| 117 | + * Report an exception, with default error code, with userInfo. Useful in catch-all scenarios |
| 118 | + * where it's unclear why the error occurred. |
| 119 | + * |
| 120 | + * @param throwable Throwable |
| 121 | + * @param userInfo WritableMap |
| 122 | + */ |
| 123 | + @Override |
| 124 | + public void reject(Throwable throwable, WritableMap userInfo) { |
| 125 | + reject(/*Code*/ null, /*Message*/ null, throwable, userInfo); |
| 126 | + } |
| 127 | + |
| 128 | + /** |
| 129 | + * Reject with a code and userInfo WritableMap. |
| 130 | + * |
| 131 | + * @param code String |
| 132 | + * @param userInfo WritableMap |
| 133 | + */ |
| 134 | + @Override |
| 135 | + public void reject(String code, WritableMap userInfo) { |
| 136 | + reject(code, /*Message*/ null, /*Throwable*/ null, userInfo); |
| 137 | + } |
| 138 | + |
| 139 | + /** |
| 140 | + * Report an exception with a custom code and userInfo. |
| 141 | + * |
| 142 | + * @param code String |
| 143 | + * @param throwable Throwable |
| 144 | + * @param userInfo WritableMap |
| 145 | + */ |
| 146 | + @Override |
| 147 | + public void reject(String code, @Nullable Throwable throwable, WritableMap userInfo) { |
| 148 | + reject(code, /*Message*/ null, throwable, userInfo); |
| 149 | + } |
| 150 | + |
| 151 | + /** |
| 152 | + * Report an error with a custom code, error message and userInfo, an error not caused by an |
| 153 | + * exception. |
| 154 | + * |
| 155 | + * @param code String |
| 156 | + * @param message String |
| 157 | + * @param userInfo WritableMap |
| 158 | + */ |
| 159 | + @Override |
| 160 | + public void reject(String code, @Nullable String message, WritableMap userInfo) { |
| 161 | + reject(code, message, /*Throwable*/ null, userInfo); |
| 162 | + } |
| 163 | + |
| 164 | + /** |
| 165 | + * Report an exception with a custom code, error message and userInfo. |
| 166 | + * |
| 167 | + * @param code String |
| 168 | + * @param message String |
| 169 | + * @param throwable Throwable |
| 170 | + * @param userInfo WritableMap |
| 171 | + */ |
| 172 | + @Override |
| 173 | + public void reject( |
| 174 | + @Nullable String code, |
| 175 | + @Nullable String message, |
| 176 | + @Nullable Throwable throwable, |
| 177 | + @Nullable WritableMap userInfo) { |
| 178 | + if (mReject == null) { |
| 179 | + mResolve = null; |
| 180 | + return; |
| 181 | + } |
| 182 | + |
| 183 | + WritableNativeMap errorInfo = new WritableNativeMap(); |
| 184 | + |
| 185 | + if (code == null) { |
| 186 | + errorInfo.putString(ERROR_MAP_KEY_CODE, ERROR_DEFAULT_CODE); |
| 187 | + } else { |
| 188 | + errorInfo.putString(ERROR_MAP_KEY_CODE, code); |
| 189 | + } |
| 190 | + |
| 191 | + // Use the custom message if provided otherwise use the throwable message. |
| 192 | + if (message != null) { |
| 193 | + errorInfo.putString(ERROR_MAP_KEY_MESSAGE, message); |
| 194 | + } else if (throwable != null) { |
| 195 | + String throwableMessage = throwable.getMessage(); |
| 196 | + // Fallback to the trowable name, so we record some useful information |
| 197 | + if (throwableMessage == null || throwableMessage.isEmpty()) { |
| 198 | + throwableMessage = throwable.getClass().getCanonicalName(); |
| 199 | + } |
| 200 | + errorInfo.putString(ERROR_MAP_KEY_MESSAGE, throwableMessage); |
| 201 | + } else { |
| 202 | + // The JavaScript side expects a map with at least an error message. |
| 203 | + // /Libraries/BatchedBridge/NativeModules.js -> createErrorFromErrorData |
| 204 | + // TYPE: (errorData: { message: string }) |
| 205 | + errorInfo.putString(ERROR_MAP_KEY_MESSAGE, ERROR_DEFAULT_MESSAGE); |
| 206 | + } |
| 207 | + |
| 208 | + // For consistency with iOS ensure userInfo key exists, even if we null it. |
| 209 | + // iOS: /React/Base/RCTUtils.m -> RCTJSErrorFromCodeMessageAndNSError |
| 210 | + if (userInfo != null) { |
| 211 | + errorInfo.putMap(ERROR_MAP_KEY_USER_INFO, userInfo); |
| 212 | + } else { |
| 213 | + errorInfo.putNull(ERROR_MAP_KEY_USER_INFO); |
| 214 | + } |
| 215 | + |
| 216 | + // Attach a nativeStackAndroid array if a throwable was passed |
| 217 | + // this matches iOS behavior - iOS adds a `nativeStackIOS` property |
| 218 | + // iOS: /React/Base/RCTUtils.m -> RCTJSErrorFromCodeMessageAndNSError |
| 219 | + if (throwable != null) { |
| 220 | + errorInfo.putString(ERROR_MAP_KEY_NAME, throwable.getClass().getCanonicalName()); |
| 221 | + |
| 222 | + StackTraceElement[] stackTrace = throwable.getStackTrace(); |
| 223 | + WritableNativeArray nativeStackAndroid = new WritableNativeArray(); |
| 224 | + |
| 225 | + // Build an an Array of StackFrames to match JavaScript: |
| 226 | + // iOS: /Libraries/Core/Devtools/parseErrorStack.js -> StackFrame |
| 227 | + for (int i = 0; i < stackTrace.length && i < ERROR_STACK_FRAME_LIMIT; i++) { |
| 228 | + StackTraceElement frame = stackTrace[i]; |
| 229 | + WritableMap frameMap = new WritableNativeMap(); |
| 230 | + // NOTE: no column number exists StackTraceElement |
| 231 | + frameMap.putString(STACK_FRAME_KEY_CLASS, frame.getClassName()); |
| 232 | + frameMap.putString(STACK_FRAME_KEY_FILE, frame.getFileName()); |
| 233 | + frameMap.putInt(STACK_FRAME_KEY_LINE_NUMBER, frame.getLineNumber()); |
| 234 | + frameMap.putString(STACK_FRAME_KEY_METHOD_NAME, frame.getMethodName()); |
| 235 | + nativeStackAndroid.pushMap(frameMap); |
| 236 | + } |
| 237 | + |
| 238 | + errorInfo.putArray(ERROR_MAP_KEY_NATIVE_STACK, nativeStackAndroid); |
| 239 | + } else { |
| 240 | + errorInfo.putArray(ERROR_MAP_KEY_NATIVE_STACK, new WritableNativeArray()); |
| 241 | + } |
| 242 | + |
| 243 | + mReject.invoke(errorInfo); |
| 244 | + mResolve = null; |
| 245 | + mReject = null; |
| 246 | + } |
| 247 | + |
| 248 | + /* ------------ |
| 249 | + * Deprecated |
| 250 | + * ------------ */ |
| 251 | + |
| 252 | + @Override |
| 253 | + @Deprecated |
| 254 | + public void reject(String message) { |
| 255 | + reject(/*Code*/ null, message, /*Throwable*/ null, /*WritableMap*/ null); |
| 256 | + } |
| 257 | +} |
0 commit comments