From b86a6e3b44a63e92cf3a7976d2fa26c4bf412df1 Mon Sep 17 00:00:00 2001 From: David Aurelio Date: Thu, 29 Oct 2015 04:11:39 -0700 Subject: [PATCH] Add support for async bridged methods to android Differential Revision: D2595414 fb-gh-sync-id: 3b44ce1737bdd1e0861a285a45976631a57ab3b5 --- .../facebook/react/bridge/BaseJavaModule.java | 134 +++++++++++++----- .../facebook/react/bridge/NativeModule.java | 1 + .../react/bridge/NativeModuleRegistry.java | 1 + .../com/facebook/react/bridge/Promise.java | 22 +++ .../facebook/react/bridge/PromiseImpl.java | 45 ++++++ 5 files changed, 167 insertions(+), 36 deletions(-) create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/Promise.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/PromiseImpl.java diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/BaseJavaModule.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/BaseJavaModule.java index 46c46c90098a0c..d1e7eae9e7865f 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/BaseJavaModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/BaseJavaModule.java @@ -11,6 +11,7 @@ import com.fasterxml.jackson.core.JsonGenerator; +import com.facebook.infer.annotation.Assertions; import com.facebook.systrace.Systrace; import javax.annotation.Nullable; @@ -46,8 +47,16 @@ * with the same name. */ public abstract class BaseJavaModule implements NativeModule { - private interface ArgumentExtractor { - @Nullable Object extractArgument( + // taken from Libraries/Utilities/MessageQueue.js + static final public String METHOD_TYPE_REMOTE = "remote"; + static final public String METHOD_TYPE_REMOTE_ASYNC = "remoteAsync"; + + private static abstract class ArgumentExtractor { + public int getJSArgumentsNeeded() { + return 1; + } + + public abstract @Nullable Object extractArgument( CatalystInstance catalystInstance, ReadableNativeArray jsArguments, int atIndex); } @@ -129,38 +138,30 @@ public Object extractArgument( } }; - private static ArgumentExtractor[] buildArgumentExtractors(Class[] parameterTypes) { - ArgumentExtractor[] argumentExtractors = new ArgumentExtractor[parameterTypes.length]; - for (int i = 0; i < parameterTypes.length; i++) { - Class argumentClass = parameterTypes[i]; - if (argumentClass == Boolean.class || argumentClass == boolean.class) { - argumentExtractors[i] = ARGUMENT_EXTRACTOR_BOOLEAN; - } else if (argumentClass == Integer.class || argumentClass == int.class) { - argumentExtractors[i] = ARGUMENT_EXTRACTOR_INTEGER; - } else if (argumentClass == Double.class || argumentClass == double.class) { - argumentExtractors[i] = ARGUMENT_EXTRACTOR_DOUBLE; - } else if (argumentClass == Float.class || argumentClass == float.class) { - argumentExtractors[i] = ARGUMENT_EXTRACTOR_FLOAT; - } else if (argumentClass == String.class) { - argumentExtractors[i] = ARGUMENT_EXTRACTOR_STRING; - } else if (argumentClass == Callback.class) { - argumentExtractors[i] = ARGUMENT_EXTRACTOR_CALLBACK; - } else if (argumentClass == ReadableMap.class) { - argumentExtractors[i] = ARGUMENT_EXTRACTOR_MAP; - } else if (argumentClass == ReadableArray.class) { - argumentExtractors[i] = ARGUMENT_EXTRACTOR_ARRAY; - } else { - throw new RuntimeException( - "Got unknown argument class: " + argumentClass.getSimpleName()); - } + static final private ArgumentExtractor ARGUMENT_EXTRACTOR_PROMISE = new ArgumentExtractor() { + @Override + public int getJSArgumentsNeeded() { + return 2; } - return argumentExtractors; - } + + @Override + public Promise extractArgument( + CatalystInstance catalystInstance, ReadableNativeArray jsArguments, int atIndex) { + Callback resolve = (Callback) ARGUMENT_EXTRACTOR_CALLBACK + .extractArgument(catalystInstance, jsArguments, atIndex); + Callback reject = (Callback) ARGUMENT_EXTRACTOR_CALLBACK + .extractArgument(catalystInstance, jsArguments, atIndex + 1); + return new PromiseImpl(resolve, reject); + } + }; private class JavaMethod implements NativeMethod { + private Method mMethod; - private ArgumentExtractor[] mArgumentExtractors; - private Object[] mArguments; + private final ArgumentExtractor[] mArgumentExtractors; + private final Object[] mArguments; + private String mType = METHOD_TYPE_REMOTE; + private final int mJSArgumentsNeeded; public JavaMethod(Method method) { mMethod = method; @@ -168,28 +169,79 @@ public JavaMethod(Method method) { mArgumentExtractors = buildArgumentExtractors(parameterTypes); // Since native methods are invoked from a message queue executed on a single thread, it is // save to allocate only one arguments object per method that can be reused across calls - mArguments = new Object[mArgumentExtractors.length]; + mArguments = new Object[parameterTypes.length]; + mJSArgumentsNeeded = calculateJSArgumentsNeeded(); + } + + private ArgumentExtractor[] buildArgumentExtractors(Class[] paramTypes) { + ArgumentExtractor[] argumentExtractors = new ArgumentExtractor[paramTypes.length]; + for (int i = 0; i < paramTypes.length; i += argumentExtractors[i].getJSArgumentsNeeded()) { + Class argumentClass = paramTypes[i]; + if (argumentClass == Boolean.class || argumentClass == boolean.class) { + argumentExtractors[i] = ARGUMENT_EXTRACTOR_BOOLEAN; + } else if (argumentClass == Integer.class || argumentClass == int.class) { + argumentExtractors[i] = ARGUMENT_EXTRACTOR_INTEGER; + } else if (argumentClass == Double.class || argumentClass == double.class) { + argumentExtractors[i] = ARGUMENT_EXTRACTOR_DOUBLE; + } else if (argumentClass == Float.class || argumentClass == float.class) { + argumentExtractors[i] = ARGUMENT_EXTRACTOR_FLOAT; + } else if (argumentClass == String.class) { + argumentExtractors[i] = ARGUMENT_EXTRACTOR_STRING; + } else if (argumentClass == Callback.class) { + argumentExtractors[i] = ARGUMENT_EXTRACTOR_CALLBACK; + } else if (argumentClass == Promise.class) { + argumentExtractors[i] = ARGUMENT_EXTRACTOR_PROMISE; + Assertions.assertCondition( + i == paramTypes.length - 1, "Promise must be used as last parameter only"); + mType = METHOD_TYPE_REMOTE_ASYNC; + } else if (argumentClass == ReadableMap.class) { + argumentExtractors[i] = ARGUMENT_EXTRACTOR_MAP; + } else if (argumentClass == ReadableArray.class) { + argumentExtractors[i] = ARGUMENT_EXTRACTOR_ARRAY; + } else { + throw new RuntimeException( + "Got unknown argument class: " + argumentClass.getSimpleName()); + } + } + return argumentExtractors; + } + + private int calculateJSArgumentsNeeded() { + int n = 0; + for (ArgumentExtractor extractor : mArgumentExtractors) { + n += extractor.getJSArgumentsNeeded(); + } + return n; + } + + private String getAffectedRange(int startIndex, int jsArgumentsNeeded) { + return jsArgumentsNeeded > 1 ? + "" + startIndex + "-" + (startIndex + jsArgumentsNeeded - 1) : "" + startIndex; } @Override public void invoke(CatalystInstance catalystInstance, ReadableNativeArray parameters) { Systrace.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "callJavaModuleMethod"); try { - if (mArgumentExtractors.length != parameters.size()) { + if (mJSArgumentsNeeded != parameters.size()) { throw new NativeArgumentsParseException( BaseJavaModule.this.getName() + "." + mMethod.getName() + " got " + - parameters.size() + " arguments, expected " + mArgumentExtractors.length); + parameters.size() + " arguments, expected " + mJSArgumentsNeeded); } - int i = 0; + int i = 0, jsArgumentsConsumed = 0; try { for (; i < mArgumentExtractors.length; i++) { - mArguments[i] = mArgumentExtractors[i].extractArgument(catalystInstance, parameters, i); + mArguments[i] = mArgumentExtractors[i].extractArgument( + catalystInstance, parameters, jsArgumentsConsumed); + jsArgumentsConsumed += mArgumentExtractors[i].getJSArgumentsNeeded(); } } catch (UnexpectedNativeTypeException e) { throw new NativeArgumentsParseException( e.getMessage() + " (constructing arguments for " + BaseJavaModule.this.getName() + - "." + mMethod.getName() + " at argument index " + i + ")", + "." + mMethod.getName() + " at argument index " + + getAffectedRange(jsArgumentsConsumed, mArgumentExtractors[i].getJSArgumentsNeeded()) + + ")", e); } @@ -214,6 +266,16 @@ public void invoke(CatalystInstance catalystInstance, ReadableNativeArray parame Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); } } + + /** + * Determines how the method is exported in JavaScript: + * METHOD_TYPE_REMOTE for regular methods + * METHOD_TYPE_REMOTE_ASYNC for methods that return a promise object to the caller. + */ + @Override + public String getType() { + return mType; + } } @Override diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeModule.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeModule.java index 02df61959747d4..480e4218207940 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeModule.java @@ -24,6 +24,7 @@ public interface NativeModule { public static interface NativeMethod { void invoke(CatalystInstance catalystInstance, ReadableNativeArray parameters); + String getType(); } /** diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeModuleRegistry.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeModuleRegistry.java index f185118e86efd8..72b3ad66695346 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeModuleRegistry.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeModuleRegistry.java @@ -199,6 +199,7 @@ public NativeModuleRegistry build() { MethodRegistration method = module.methods.get(i); jg.writeObjectFieldStart(method.name); jg.writeNumberField("methodID", i); + jg.writeStringField("type", method.method.getType()); jg.writeEndObject(); } jg.writeEndObject(); diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/Promise.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/Promise.java new file mode 100644 index 00000000000000..c19907af83cdcd --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/Promise.java @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.bridge; + +/** + * Interface that represents a JavaScript Promise which can be passed to the native module as a + * method parameter. + * + * Methods annotated with {@link ReactMethod} that use {@link Promise} as type of the last parameter + * will be marked as "remoteAsync" and will return a promise when invoked from JavaScript. + */ +public interface Promise { + void resolve(Object value); + void reject(String reason); +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/PromiseImpl.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/PromiseImpl.java new file mode 100644 index 00000000000000..688b081028e8f3 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/PromiseImpl.java @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +/** + * Implementation of two javascript functions that can be used to resolve or reject a js promise. + */ +package com.facebook.react.bridge; + +import javax.annotation.Nullable; + +public class PromiseImpl implements Promise { + private @Nullable Callback mResolve; + private @Nullable Callback mReject; + + public PromiseImpl(@Nullable Callback resolve, @Nullable Callback reject) { + mResolve = resolve; + mReject = reject; + } + + @Override + public void resolve(Object value) { + if (mResolve != null) { + mResolve.invoke(value); + } + } + + @Override + public void reject(String reason) { + if (mReject != null) { + // The JavaScript side expects a map with at least the error message. + // It is possible to expose all kind of information. It will be available on the JS + // error instance. + // TODO(8850038): add more useful information, e.g. the native stack trace. + WritableNativeMap errorInfo = new WritableNativeMap(); + errorInfo.putString("message", reason); + mReject.invoke(errorInfo); + } + } +}