From 3b3b46d86e1b6d8d4debbab3aafaf1fd454749c0 Mon Sep 17 00:00:00 2001 From: Chris Hopman Date: Wed, 25 May 2016 17:43:52 -0700 Subject: [PATCH] Move new bridge java stuff to OSS Reviewed By: mhorowitz Differential Revision: D3300152 fbshipit-source-id: 9a76b10579bbfc5bde3a5094b99b64c38f4c1da9 --- .../src/main/java/com/facebook/react/BUCK | 49 +- .../facebook/react/XReactInstanceManager.java | 68 ++ .../react/XReactInstanceManagerImpl.java | 865 ++++++++++++++++++ .../facebook/react/cxxbridge/Arguments.java | 159 ++++ .../java/com/facebook/react/cxxbridge/BUCK | 38 + .../react/cxxbridge/CallbackImpl.java | 30 + .../react/cxxbridge/CatalystInstanceImpl.java | 461 ++++++++++ .../react/cxxbridge/CxxModuleWrapper.java | 110 +++ .../react/cxxbridge/ExecutorToken.java | 0 .../react/cxxbridge/JSBundleLoader.java | 90 ++ .../cxxbridge/JSCJavaScriptExecutor.java | 46 + .../react/cxxbridge/JavaModuleWrapper.java | 141 +++ .../react/cxxbridge/JavaScriptExecutor.java | 35 + .../react/cxxbridge/ModuleRegistryHolder.java | 28 + .../react/cxxbridge/NativeModuleRegistry.java | 138 +++ .../cxxbridge/ProxyJavaScriptExecutor.java | 67 ++ .../react/cxxbridge/ReactCallback.java | 27 + .../facebook/react/cxxbridge/ReactMarker.java | 31 + .../react/cxxbridge/SoftAssertions.java | 52 ++ .../react/cxxbridge/UiThreadUtil.java | 56 ++ .../com/facebook/react/cxxbridge/bridge.pro | 7 + 21 files changed, 2483 insertions(+), 15 deletions(-) create mode 100644 ReactAndroid/src/main/java/com/facebook/react/XReactInstanceManager.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/XReactInstanceManagerImpl.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/cxxbridge/Arguments.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/cxxbridge/BUCK create mode 100644 ReactAndroid/src/main/java/com/facebook/react/cxxbridge/CallbackImpl.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/cxxbridge/CatalystInstanceImpl.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/cxxbridge/CxxModuleWrapper.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/cxxbridge/ExecutorToken.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/cxxbridge/JSBundleLoader.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/cxxbridge/JSCJavaScriptExecutor.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/cxxbridge/JavaModuleWrapper.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/cxxbridge/JavaScriptExecutor.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/cxxbridge/ModuleRegistryHolder.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/cxxbridge/NativeModuleRegistry.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/cxxbridge/ProxyJavaScriptExecutor.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/cxxbridge/ReactCallback.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/cxxbridge/ReactMarker.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/cxxbridge/SoftAssertions.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/cxxbridge/UiThreadUtil.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/cxxbridge/bridge.pro diff --git a/ReactAndroid/src/main/java/com/facebook/react/BUCK b/ReactAndroid/src/main/java/com/facebook/react/BUCK index 0cc6b1e0d..6fcd3551c 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/BUCK +++ b/ReactAndroid/src/main/java/com/facebook/react/BUCK @@ -1,22 +1,41 @@ include_defs('//ReactAndroid/DEFS') +XREACT_SRCS = [ + 'XReactInstanceManager.java', + 'XReactInstanceManagerImpl.java', +] + +DEPS = [ + react_native_target('java/com/facebook/react/bridge:bridge'), + react_native_target('java/com/facebook/react/common:common'), + react_native_target('java/com/facebook/react/devsupport:devsupport'), + react_native_target('java/com/facebook/react/modules/core:core'), + react_native_target('java/com/facebook/react/modules/debug:debug'), + react_native_target('java/com/facebook/react/modules/systeminfo:systeminfo'), + react_native_target('java/com/facebook/react/modules/toast:toast'), + react_native_target('java/com/facebook/react/uimanager:uimanager'), + react_native_dep('java/com/facebook/systrace:systrace'), + react_native_dep('libraries/fbcore/src/main/java/com/facebook/common/logging:logging'), + react_native_dep('libraries/soloader/java/com/facebook/soloader:soloader'), + react_native_dep('third-party/java/infer-annotations:infer-annotations'), + react_native_dep('third-party/java/jsr-305:jsr-305'), +] + android_library( name = 'react', - srcs = glob(['*.java']), - deps = [ - react_native_target('java/com/facebook/react/bridge:bridge'), - react_native_target('java/com/facebook/react/common:common'), - react_native_target('java/com/facebook/react/devsupport:devsupport'), - react_native_target('java/com/facebook/react/modules/core:core'), - react_native_target('java/com/facebook/react/modules/debug:debug'), - react_native_target('java/com/facebook/react/modules/systeminfo:systeminfo'), - react_native_target('java/com/facebook/react/modules/toast:toast'), - react_native_target('java/com/facebook/react/uimanager:uimanager'), - react_native_dep('java/com/facebook/systrace:systrace'), - react_native_dep('libraries/fbcore/src/main/java/com/facebook/common/logging:logging'), - react_native_dep('libraries/soloader/java/com/facebook/soloader:soloader'), - react_native_dep('third-party/java/infer-annotations:infer-annotations'), - react_native_dep('third-party/java/jsr-305:jsr-305'), + srcs = glob(['*.java'], excludes=XREACT_SRCS), + deps = DEPS, + visibility = [ + 'PUBLIC', + ], +) + +android_library( + name = 'xreact', + srcs = XREACT_SRCS, + deps = DEPS + [ + ':react', + react_native_target('java/com/facebook/react/cxxbridge:bridge'), ], visibility = [ 'PUBLIC', diff --git a/ReactAndroid/src/main/java/com/facebook/react/XReactInstanceManager.java b/ReactAndroid/src/main/java/com/facebook/react/XReactInstanceManager.java new file mode 100644 index 000000000..9ac350808 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/XReactInstanceManager.java @@ -0,0 +1,68 @@ +/** + * 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; + +import com.facebook.infer.annotation.Assertions; +import com.facebook.react.uimanager.UIImplementationProvider; + +public abstract class XReactInstanceManager { + /** + * Creates a builder that is capable of creating an instance of {@link XReactInstanceManagerImpl}. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Builder class for {@link XReactInstanceManagerImpl} + */ + public static class Builder extends ReactInstanceManager.Builder { + /** + * Instantiates a new {@link ReactInstanceManagerImpl}. + * Before calling {@code build}, the following must be called: + * + */ + public ReactInstanceManager build() { + Assertions.assertCondition( + mUseDeveloperSupport || mJSBundleFile != null, + "JS Bundle File has to be provided when dev support is disabled"); + + Assertions.assertCondition( + mJSMainModuleName != null || mJSBundleFile != null, + "Either MainModuleName or JS Bundle File needs to be provided"); + + if (mUIImplementationProvider == null) { + // create default UIImplementationProvider if the provided one is null. + mUIImplementationProvider = new UIImplementationProvider(); + } + + return new XReactInstanceManagerImpl( + Assertions.assertNotNull( + mApplication, + "Application property has not been set with this builder"), + mCurrentActivity, + mDefaultHardwareBackBtnHandler, + mJSBundleFile, + mJSMainModuleName, + mPackages, + mUseDeveloperSupport, + mBridgeIdleDebugListener, + Assertions.assertNotNull(mInitialLifecycleState, "Initial lifecycle state was not set"), + mUIImplementationProvider, + mNativeModuleCallExceptionHandler, + mJSCConfig); + } + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/XReactInstanceManagerImpl.java b/ReactAndroid/src/main/java/com/facebook/react/XReactInstanceManagerImpl.java new file mode 100644 index 000000000..0cc29def3 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/XReactInstanceManagerImpl.java @@ -0,0 +1,865 @@ +/** + * 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; + +import javax.annotation.Nullable; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; + +import android.app.Activity; +import android.app.Application; +import android.content.Context; +import android.content.Intent; +import android.os.AsyncTask; +import android.os.Bundle; +import android.view.View; + +import com.facebook.common.logging.FLog; +import com.facebook.infer.annotation.Assertions; +import com.facebook.react.bridge.CatalystInstance; +import com.facebook.react.bridge.JavaJSExecutor; +import com.facebook.react.bridge.JavaScriptModule; +import com.facebook.react.bridge.JavaScriptModuleRegistry; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.NativeModuleCallExceptionHandler; +import com.facebook.react.bridge.NotThreadSafeBridgeIdleDebugListener; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.ReactMarker; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.bridge.WritableNativeMap; +import com.facebook.react.bridge.queue.ReactQueueConfigurationSpec; +import com.facebook.react.common.ApplicationHolder; +import com.facebook.react.common.ReactConstants; +import com.facebook.react.common.annotations.VisibleForTesting; +import com.facebook.react.cxxbridge.Arguments; +import com.facebook.react.cxxbridge.CatalystInstanceImpl; +import com.facebook.react.cxxbridge.JSBundleLoader; +import com.facebook.react.cxxbridge.JSCJavaScriptExecutor; +import com.facebook.react.cxxbridge.JavaScriptExecutor; +import com.facebook.react.cxxbridge.NativeModuleRegistry; +import com.facebook.react.cxxbridge.ProxyJavaScriptExecutor; +import com.facebook.react.cxxbridge.UiThreadUtil; +import com.facebook.react.devsupport.DevServerHelper; +import com.facebook.react.devsupport.DevSupportManager; +import com.facebook.react.devsupport.DevSupportManagerFactory; +import com.facebook.react.devsupport.ReactInstanceDevCommandsHandler; +import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler; +import com.facebook.react.modules.core.DeviceEventManagerModule; +import com.facebook.react.uimanager.AppRegistry; +import com.facebook.react.uimanager.DisplayMetricsHolder; +import com.facebook.react.uimanager.UIImplementationProvider; +import com.facebook.react.uimanager.UIManagerModule; +import com.facebook.react.uimanager.ViewManager; +import com.facebook.soloader.SoLoader; +import com.facebook.systrace.Systrace; + +import static com.facebook.react.bridge.ReactMarkerConstants.BUILD_JS_MODULE_CONFIG_END; +import static com.facebook.react.bridge.ReactMarkerConstants.BUILD_JS_MODULE_CONFIG_START; +import static com.facebook.react.bridge.ReactMarkerConstants.BUILD_NATIVE_MODULE_REGISTRY_END; +import static com.facebook.react.bridge.ReactMarkerConstants.BUILD_NATIVE_MODULE_REGISTRY_START; +import static com.facebook.react.bridge.ReactMarkerConstants.CREATE_CATALYST_INSTANCE_END; +import static com.facebook.react.bridge.ReactMarkerConstants.CREATE_CATALYST_INSTANCE_START; +import static com.facebook.react.bridge.ReactMarkerConstants.CREATE_REACT_CONTEXT_START; +import static com.facebook.react.bridge.ReactMarkerConstants.PROCESS_PACKAGES_END; +import static com.facebook.react.bridge.ReactMarkerConstants.PROCESS_PACKAGES_START; +import static com.facebook.react.bridge.ReactMarkerConstants.RUN_JS_BUNDLE_END; +import static com.facebook.react.bridge.ReactMarkerConstants.RUN_JS_BUNDLE_START; + +/** + * This class is managing instances of {@link CatalystInstance}. It expose a way to configure + * catalyst instance using {@link ReactPackage} and keeps track of the lifecycle of that + * instance. It also sets up connection between the instance and developers support functionality + * of the framework. + * + * An instance of this manager is required to start JS application in {@link ReactRootView} (see + * {@link ReactRootView#startReactApplication} for more info). + * + * The lifecycle of the instance of {@link XReactInstanceManagerImpl} should be bound to the + * activity that owns the {@link ReactRootView} that is used to render react application using this + * instance manager (see {@link ReactRootView#startReactApplication}). It's required to pass owning + * activity's lifecycle events to the instance manager (see {@link #onHostPause}, {@link + * #onHostDestroy} and {@link #onHostResume}). + * + * To instantiate an instance of this class use {@link #builder}. + */ +/* package */ class XReactInstanceManagerImpl extends ReactInstanceManager { + + /* should only be accessed from main thread (UI thread) */ + private final List mAttachedRootViews = new ArrayList<>(); + private LifecycleState mLifecycleState; + private @Nullable ReactContextInitParams mPendingReactContextInitParams; + private @Nullable ReactContextInitAsyncTask mReactContextInitAsyncTask; + + /* accessed from any thread */ + private @Nullable String mJSBundleFile; /* path to JS bundle on file system */ + private final @Nullable String mJSMainModuleName; /* path to JS bundle root on packager server */ + private final List mPackages; + private final DevSupportManager mDevSupportManager; + private final boolean mUseDeveloperSupport; + private final @Nullable NotThreadSafeBridgeIdleDebugListener mBridgeIdleDebugListener; + private @Nullable volatile ReactContext mCurrentReactContext; + private final Context mApplicationContext; + private @Nullable DefaultHardwareBackBtnHandler mDefaultBackButtonImpl; + private String mSourceUrl; + private @Nullable Activity mCurrentActivity; + private final Collection mReactInstanceEventListeners = + Collections.synchronizedSet(new HashSet()); + private volatile boolean mHasStartedCreatingInitialContext = false; + private final UIImplementationProvider mUIImplementationProvider; + private final MemoryPressureRouter mMemoryPressureRouter; + private final @Nullable NativeModuleCallExceptionHandler mNativeModuleCallExceptionHandler; + private final @Nullable JSCConfig mJSCConfig; + + private final ReactInstanceDevCommandsHandler mDevInterface = + new ReactInstanceDevCommandsHandler() { + + @Override + public void onReloadWithJSDebugger(JavaJSExecutor.Factory jsExecutorFactory) { + XReactInstanceManagerImpl.this.onReloadWithJSDebugger(jsExecutorFactory); + } + + @Override + public void onJSBundleLoadedFromServer() { + XReactInstanceManagerImpl.this.onJSBundleLoadedFromServer(); + } + + @Override + public void toggleElementInspector() { + XReactInstanceManagerImpl.this.toggleElementInspector(); + } + }; + + private final DefaultHardwareBackBtnHandler mBackBtnHandler = + new DefaultHardwareBackBtnHandler() { + @Override + public void invokeDefaultOnBackPressed() { + XReactInstanceManagerImpl.this.invokeDefaultOnBackPressed(); + } + }; + + private class ReactContextInitParams { + private final JavaScriptExecutor.Factory mJsExecutorFactory; + private final JSBundleLoader mJsBundleLoader; + + public ReactContextInitParams( + JavaScriptExecutor.Factory jsExecutorFactory, + JSBundleLoader jsBundleLoader) { + mJsExecutorFactory = Assertions.assertNotNull(jsExecutorFactory); + mJsBundleLoader = Assertions.assertNotNull(jsBundleLoader); + } + + public JavaScriptExecutor.Factory getJsExecutorFactory() { + return mJsExecutorFactory; + } + + public JSBundleLoader getJsBundleLoader() { + return mJsBundleLoader; + } + } + + /* + * Task class responsible for (re)creating react context in the background. These tasks can only + * be executing one at time, see {@link #recreateReactContextInBackground()}. + */ + private final class ReactContextInitAsyncTask extends + AsyncTask> { + @Override + protected void onPreExecute() { + if (mCurrentReactContext != null) { + tearDownReactContext(mCurrentReactContext); + mCurrentReactContext = null; + } + } + + @Override + protected Result doInBackground(ReactContextInitParams... params) { + Assertions.assertCondition(params != null && params.length > 0 && params[0] != null); + try { + JavaScriptExecutor jsExecutor = params[0].getJsExecutorFactory().create(); + return Result.of(createReactContext(jsExecutor, params[0].getJsBundleLoader())); + } catch (Exception e) { + // Pass exception to onPostExecute() so it can be handled on the main thread + return Result.of(e); + } + } + + @Override + protected void onPostExecute(Result result) { + try { + setupReactContext(result.get()); + } catch (Exception e) { + mDevSupportManager.handleException(e); + } finally { + mReactContextInitAsyncTask = null; + } + + // Handle enqueued request to re-initialize react context. + if (mPendingReactContextInitParams != null) { + recreateReactContextInBackground( + mPendingReactContextInitParams.getJsExecutorFactory(), + mPendingReactContextInitParams.getJsBundleLoader()); + mPendingReactContextInitParams = null; + } + } + + @Override + protected void onCancelled(Result reactApplicationContextResult) { + try { + mMemoryPressureRouter.destroy(reactApplicationContextResult.get()); + } catch (Exception e) { + FLog.w(ReactConstants.TAG, "Caught exception after cancelling react context init", e); + } finally { + mReactContextInitAsyncTask = null; + } + } + } + + private static class Result { + @Nullable private final T mResult; + @Nullable private final Exception mException; + + public static Result of(U result) { + return new Result(result); + } + + public static Result of(Exception exception) { + return new Result<>(exception); + } + + private Result(T result) { + mException = null; + mResult = result; + } + + private Result(Exception exception) { + mException = exception; + mResult = null; + } + + public T get() throws Exception { + if (mException != null) { + throw mException; + } + + Assertions.assertNotNull(mResult); + + return mResult; + } + } + + /* package */ XReactInstanceManagerImpl( + Context applicationContext, + @Nullable Activity currentActivity, + @Nullable DefaultHardwareBackBtnHandler defaultHardwareBackBtnHandler, + @Nullable String jsBundleFile, + @Nullable String jsMainModuleName, + List packages, + boolean useDeveloperSupport, + @Nullable NotThreadSafeBridgeIdleDebugListener bridgeIdleDebugListener, + LifecycleState initialLifecycleState, + UIImplementationProvider uiImplementationProvider, + NativeModuleCallExceptionHandler nativeModuleCallExceptionHandler, + @Nullable JSCConfig jscConfig) { + initializeSoLoaderIfNecessary(applicationContext); + + // TODO(9577825): remove this + ApplicationHolder.setApplication((Application) applicationContext.getApplicationContext()); + DisplayMetricsHolder.initDisplayMetricsIfNotInitialized(applicationContext); + + mApplicationContext = applicationContext; + mCurrentActivity = currentActivity; + mDefaultBackButtonImpl = defaultHardwareBackBtnHandler; + mJSBundleFile = jsBundleFile; + mJSMainModuleName = jsMainModuleName; + mPackages = packages; + mUseDeveloperSupport = useDeveloperSupport; + mDevSupportManager = DevSupportManagerFactory.create( + applicationContext, + mDevInterface, + mJSMainModuleName, + useDeveloperSupport); + mBridgeIdleDebugListener = bridgeIdleDebugListener; + mLifecycleState = initialLifecycleState; + mUIImplementationProvider = uiImplementationProvider; + mMemoryPressureRouter = new MemoryPressureRouter(applicationContext); + mNativeModuleCallExceptionHandler = nativeModuleCallExceptionHandler; + mJSCConfig = jscConfig; + } + + @Override + public DevSupportManager getDevSupportManager() { + return mDevSupportManager; + } + + @Override + public MemoryPressureRouter getMemoryPressureRouter() { + return mMemoryPressureRouter; + } + + private static void initializeSoLoaderIfNecessary(Context applicationContext) { + // Call SoLoader.initialize here, this is required for apps that does not use exopackage and + // does not use SoLoader for loading other native code except from the one used by React Native + // This way we don't need to require others to have additional initialization code and to + // subclass android.app.Application. + + // Method SoLoader.init is idempotent, so if you wish to use native exopackage, just call + // SoLoader.init with appropriate args before initializing XReactInstanceManagerImpl + SoLoader.init(applicationContext, /* native exopackage */ false); + } + + /** + * Trigger react context initialization asynchronously in a background async task. This enables + * applications to pre-load the application JS, and execute global code before + * {@link ReactRootView} is available and measured. This should only be called the first time the + * application is set up, which is enforced to keep developers from accidentally creating their + * application multiple times without realizing it. + * + * Called from UI thread. + */ + @Override + public void createReactContextInBackground() { + Assertions.assertCondition( + !mHasStartedCreatingInitialContext, + "createReactContextInBackground should only be called when creating the react " + + "application for the first time. When reloading JS, e.g. from a new file, explicitly" + + "use recreateReactContextInBackground"); + + mHasStartedCreatingInitialContext = true; + recreateReactContextInBackgroundInner(); + } + + /** + * Recreate the react application and context. This should be called if configuration has + * changed or the developer has requested the app to be reloaded. It should only be called after + * an initial call to createReactContextInBackground. + * + * Called from UI thread. + */ + public void recreateReactContextInBackground() { + Assertions.assertCondition( + mHasStartedCreatingInitialContext, + "recreateReactContextInBackground should only be called after the initial " + + "createReactContextInBackground call."); + recreateReactContextInBackgroundInner(); + } + + private void recreateReactContextInBackgroundInner() { + UiThreadUtil.assertOnUiThread(); + + if (mUseDeveloperSupport && mJSMainModuleName != null) { + if (mDevSupportManager.hasUpToDateJSBundleInCache()) { + // If there is a up-to-date bundle downloaded from server, always use that + onJSBundleLoadedFromServer(); + } else if (mJSBundleFile == null) { + mDevSupportManager.handleReloadJS(); + } else { + mDevSupportManager.isPackagerRunning( + new DevServerHelper.PackagerStatusCallback() { + @Override + public void onPackagerStatusFetched(final boolean packagerIsRunning) { + UiThreadUtil.runOnUiThread( + new Runnable() { + @Override + public void run() { + if (packagerIsRunning) { + mDevSupportManager.handleReloadJS(); + } else { + recreateReactContextInBackgroundFromBundleFile(); + } + } + }); + } + }); + } + return; + } + + recreateReactContextInBackgroundFromBundleFile(); + } + + private void recreateReactContextInBackgroundFromBundleFile() { + recreateReactContextInBackground( + new JSCJavaScriptExecutor.Factory( + mJSCConfig == null ? new WritableNativeMap() : mJSCConfig.getConfigMap()), + JSBundleLoader.createFileLoader(mApplicationContext, mJSBundleFile)); + } + + /** + * @return whether createReactContextInBackground has been called. Will return false after + * onDestroy until a new initial context has been created. + */ + public boolean hasStartedCreatingInitialContext() { + return mHasStartedCreatingInitialContext; + } + + /** + * This method will give JS the opportunity to consume the back button event. If JS does not + * consume the event, mDefaultBackButtonImpl will be invoked at the end of the round trip to JS. + */ + @Override + public void onBackPressed() { + UiThreadUtil.assertOnUiThread(); + ReactContext reactContext = mCurrentReactContext; + if (mCurrentReactContext == null) { + // Invoke without round trip to JS. + FLog.w(ReactConstants.TAG, "Instance detached from instance manager"); + invokeDefaultOnBackPressed(); + } else { + DeviceEventManagerModule deviceEventManagerModule = + Assertions.assertNotNull(reactContext).getNativeModule(DeviceEventManagerModule.class); + deviceEventManagerModule.emitHardwareBackPressed(); + } + } + + private void invokeDefaultOnBackPressed() { + UiThreadUtil.assertOnUiThread(); + if (mDefaultBackButtonImpl != null) { + mDefaultBackButtonImpl.invokeDefaultOnBackPressed(); + } + } + + private void toggleElementInspector() { + if (mCurrentReactContext != null) { + mCurrentReactContext + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit("toggleElementInspector", null); + } + } + + @Override + public void onHostPause() { + UiThreadUtil.assertOnUiThread(); + + mDefaultBackButtonImpl = null; + if (mUseDeveloperSupport) { + mDevSupportManager.setDevSupportEnabled(false); + } + + moveToBeforeResumeLifecycleState(); + mCurrentActivity = null; + } + + /** + * Use this method when the activity resumes to enable invoking the back button directly from JS. + * + * This method retains an instance to provided mDefaultBackButtonImpl. Thus it's important to pass + * from the activity instance that owns this particular instance of {@link + * XReactInstanceManagerImpl}, so that once this instance receive {@link #onHostDestroy} event it + * will clear the reference to that defaultBackButtonImpl. + * + * @param defaultBackButtonImpl a {@link DefaultHardwareBackBtnHandler} from an Activity that owns + * this instance of {@link XReactInstanceManagerImpl}. + */ + @Override + public void onHostResume(Activity activity, DefaultHardwareBackBtnHandler defaultBackButtonImpl) { + UiThreadUtil.assertOnUiThread(); + + mDefaultBackButtonImpl = defaultBackButtonImpl; + if (mUseDeveloperSupport) { + mDevSupportManager.setDevSupportEnabled(true); + } + + mCurrentActivity = activity; + moveToResumedLifecycleState(false); + } + + @Override + public void onHostDestroy() { + UiThreadUtil.assertOnUiThread(); + + if (mUseDeveloperSupport) { + mDevSupportManager.setDevSupportEnabled(false); + } + + moveToBeforeCreateLifecycleState(); + mCurrentActivity = null; + } + + @Override + public void destroy() { + UiThreadUtil.assertOnUiThread(); + + if (mUseDeveloperSupport) { + mDevSupportManager.setDevSupportEnabled(false); + } + + moveToBeforeCreateLifecycleState(); + + if (mReactContextInitAsyncTask != null) { + mReactContextInitAsyncTask.cancel(true); + } + + mMemoryPressureRouter.destroy(mApplicationContext); + + if (mCurrentReactContext != null) { + mCurrentReactContext.destroy(); + mCurrentReactContext = null; + mHasStartedCreatingInitialContext = false; + } + mCurrentActivity = null; + } + + private void moveToResumedLifecycleState(boolean force) { + if (mCurrentReactContext != null) { + // we currently don't have an onCreate callback so we call onResume for both transitions + if (force || + mLifecycleState == LifecycleState.BEFORE_RESUME || + mLifecycleState == LifecycleState.BEFORE_CREATE) { + mCurrentReactContext.onHostResume(mCurrentActivity); + } + } + mLifecycleState = LifecycleState.RESUMED; + } + + private void moveToBeforeResumeLifecycleState() { + if (mCurrentReactContext != null) { + if (mLifecycleState == LifecycleState.BEFORE_CREATE) { + mCurrentReactContext.onHostResume(mCurrentActivity); + mCurrentReactContext.onHostPause(); + } else if (mLifecycleState == LifecycleState.RESUMED) { + mCurrentReactContext.onHostPause(); + } + } + mLifecycleState = LifecycleState.BEFORE_RESUME; + } + + private void moveToBeforeCreateLifecycleState() { + if (mCurrentReactContext != null) { + if (mLifecycleState == LifecycleState.RESUMED) { + mCurrentReactContext.onHostPause(); + mLifecycleState = LifecycleState.BEFORE_RESUME; + } + if (mLifecycleState == LifecycleState.BEFORE_RESUME) { + mCurrentReactContext.onHostDestroy(); + } + } + mLifecycleState = LifecycleState.BEFORE_CREATE; + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (mCurrentReactContext != null) { + mCurrentReactContext.onActivityResult(requestCode, resultCode, data); + } + } + + @Override + public void showDevOptionsDialog() { + UiThreadUtil.assertOnUiThread(); + mDevSupportManager.showDevOptionsDialog(); + } + + /** + * Get the URL where the last bundle was loaded from. + */ + @Override + public String getSourceUrl() { + return Assertions.assertNotNull(mSourceUrl); + } + + /** + * Attach given {@param rootView} to a catalyst instance manager and start JS application using + * JS module provided by {@link ReactRootView#getJSModuleName}. If the react context is currently + * being (re)-created, or if react context has not been created yet, the JS application associated + * with the provided root view will be started asynchronously, i.e this method won't block. + * This view will then be tracked by this manager and in case of catalyst instance restart it will + * be re-attached. + */ + @Override + public void attachMeasuredRootView(ReactRootView rootView) { + UiThreadUtil.assertOnUiThread(); + mAttachedRootViews.add(rootView); + + // If react context is being created in the background, JS application will be started + // automatically when creation completes, as root view is part of the attached root view list. + if (mReactContextInitAsyncTask == null && mCurrentReactContext != null) { + attachMeasuredRootViewToInstance(rootView, mCurrentReactContext.getCatalystInstance()); + } + } + + /** + * Detach given {@param rootView} from current catalyst instance. It's safe to call this method + * multiple times on the same {@param rootView} - in that case view will be detached with the + * first call. + */ + @Override + public void detachRootView(ReactRootView rootView) { + UiThreadUtil.assertOnUiThread(); + if (mAttachedRootViews.remove(rootView)) { + if (mCurrentReactContext != null && mCurrentReactContext.hasActiveCatalystInstance()) { + detachViewFromInstance(rootView, mCurrentReactContext.getCatalystInstance()); + } + } + } + + /** + * Uses configured {@link ReactPackage} instances to create all view managers + */ + @Override + public List createAllViewManagers( + ReactApplicationContext catalystApplicationContext) { + Systrace.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "createAllViewManagers"); + try { + List allViewManagers = new ArrayList<>(); + for (ReactPackage reactPackage : mPackages) { + allViewManagers.addAll(reactPackage.createViewManagers(catalystApplicationContext)); + } + return allViewManagers; + } finally { + Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); + } + } + + @Override + public void addReactInstanceEventListener(ReactInstanceEventListener listener) { + mReactInstanceEventListeners.add(listener); + } + + @Override + public void removeReactInstanceEventListener(ReactInstanceEventListener listener) { + mReactInstanceEventListeners.remove(listener); + } + + @VisibleForTesting + @Override + public @Nullable ReactContext getCurrentReactContext() { + return mCurrentReactContext; + } + + @Override + public LifecycleState getLifecycleState() { + return mLifecycleState; + } + + private void onReloadWithJSDebugger(JavaJSExecutor.Factory jsExecutorFactory) { + recreateReactContextInBackground( + new ProxyJavaScriptExecutor.Factory(jsExecutorFactory), + JSBundleLoader.createRemoteDebuggerBundleLoader( + mDevSupportManager.getJSBundleURLForRemoteDebugging(), + mDevSupportManager.getSourceUrl())); + } + + private void onJSBundleLoadedFromServer() { + recreateReactContextInBackground( + new JSCJavaScriptExecutor.Factory( + mJSCConfig == null ? new WritableNativeMap() : mJSCConfig.getConfigMap()), + JSBundleLoader.createCachedBundleFromNetworkLoader( + mDevSupportManager.getSourceUrl(), + mDevSupportManager.getDownloadedJSBundleFile())); + } + + private void recreateReactContextInBackground( + JavaScriptExecutor.Factory jsExecutorFactory, + JSBundleLoader jsBundleLoader) { + UiThreadUtil.assertOnUiThread(); + + ReactContextInitParams initParams = + new ReactContextInitParams(jsExecutorFactory, jsBundleLoader); + if (mReactContextInitAsyncTask == null) { + // No background task to create react context is currently running, create and execute one. + mReactContextInitAsyncTask = new ReactContextInitAsyncTask(); + mReactContextInitAsyncTask.execute(initParams); + } else { + // Background task is currently running, queue up most recent init params to recreate context + // once task completes. + mPendingReactContextInitParams = initParams; + } + } + + private void setupReactContext(ReactApplicationContext reactContext) { + UiThreadUtil.assertOnUiThread(); + Assertions.assertCondition(mCurrentReactContext == null); + mCurrentReactContext = Assertions.assertNotNull(reactContext); + CatalystInstance catalystInstance = + Assertions.assertNotNull(reactContext.getCatalystInstance()); + + catalystInstance.initialize(); + mDevSupportManager.onNewReactContextCreated(reactContext); + mMemoryPressureRouter.addMemoryPressureListener(catalystInstance); + moveReactContextToCurrentLifecycleState(); + + for (ReactRootView rootView : mAttachedRootViews) { + attachMeasuredRootViewToInstance(rootView, catalystInstance); + } + + ReactInstanceEventListener[] listeners = + new ReactInstanceEventListener[mReactInstanceEventListeners.size()]; + listeners = mReactInstanceEventListeners.toArray(listeners); + + for (ReactInstanceEventListener listener : listeners) { + listener.onReactContextInitialized(reactContext); + } + } + + private void attachMeasuredRootViewToInstance( + ReactRootView rootView, + CatalystInstance catalystInstance) { + UiThreadUtil.assertOnUiThread(); + + // Reset view content as it's going to be populated by the application content from JS + rootView.removeAllViews(); + rootView.setId(View.NO_ID); + + UIManagerModule uiManagerModule = catalystInstance.getNativeModule(UIManagerModule.class); + int rootTag = uiManagerModule.addMeasuredRootView(rootView); + @Nullable Bundle launchOptions = rootView.getLaunchOptions(); + WritableMap initialProps = Arguments.makeNativeMap(launchOptions); + String jsAppModuleName = rootView.getJSModuleName(); + + WritableNativeMap appParams = new WritableNativeMap(); + appParams.putDouble("rootTag", rootTag); + appParams.putMap("initialProps", initialProps); + catalystInstance.getJSModule(AppRegistry.class).runApplication(jsAppModuleName, appParams); + } + + private void detachViewFromInstance( + ReactRootView rootView, + CatalystInstance catalystInstance) { + UiThreadUtil.assertOnUiThread(); + catalystInstance.getJSModule(AppRegistry.class) + .unmountApplicationComponentAtRootTag(rootView.getId()); + } + + private void tearDownReactContext(ReactContext reactContext) { + UiThreadUtil.assertOnUiThread(); + if (mLifecycleState == LifecycleState.RESUMED) { + reactContext.onHostPause(); + } + for (ReactRootView rootView : mAttachedRootViews) { + detachViewFromInstance(rootView, reactContext.getCatalystInstance()); + } + reactContext.destroy(); + mDevSupportManager.onReactInstanceDestroyed(reactContext); + mMemoryPressureRouter.removeMemoryPressureListener(reactContext.getCatalystInstance()); + } + + /** + * @return instance of {@link ReactContext} configured a {@link CatalystInstance} set + */ + private ReactApplicationContext createReactContext( + JavaScriptExecutor jsExecutor, + JSBundleLoader jsBundleLoader) { + FLog.i(ReactConstants.TAG, "Creating react context."); + ReactMarker.logMarker(CREATE_REACT_CONTEXT_START); + mSourceUrl = jsBundleLoader.getSourceUrl(); + NativeModuleRegistry.Builder nativeRegistryBuilder = new NativeModuleRegistry.Builder(); + JavaScriptModuleRegistry.Builder jsModulesBuilder = new JavaScriptModuleRegistry.Builder(); + + ReactApplicationContext reactContext = new ReactApplicationContext(mApplicationContext); + if (mUseDeveloperSupport) { + reactContext.setNativeModuleCallExceptionHandler(mDevSupportManager); + } + + ReactMarker.logMarker(PROCESS_PACKAGES_START); + Systrace.beginSection( + Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, + "createAndProcessCoreModulesPackage"); + try { + CoreModulesPackage coreModulesPackage = + new CoreModulesPackage(this, mBackBtnHandler, mUIImplementationProvider); + processPackage(coreModulesPackage, reactContext, nativeRegistryBuilder, jsModulesBuilder); + } finally { + Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); + } + + // TODO(6818138): Solve use-case of native/js modules overriding + for (ReactPackage reactPackage : mPackages) { + Systrace.beginSection( + Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, + "createAndProcessCustomReactPackage"); + try { + processPackage(reactPackage, reactContext, nativeRegistryBuilder, jsModulesBuilder); + } finally { + Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); + } + } + ReactMarker.logMarker(PROCESS_PACKAGES_END); + + ReactMarker.logMarker(BUILD_NATIVE_MODULE_REGISTRY_START); + Systrace.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "buildNativeModuleRegistry"); + NativeModuleRegistry nativeModuleRegistry; + try { + nativeModuleRegistry = nativeRegistryBuilder.build(); + } finally { + Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); + ReactMarker.logMarker(BUILD_NATIVE_MODULE_REGISTRY_END); + } + + NativeModuleCallExceptionHandler exceptionHandler = mNativeModuleCallExceptionHandler != null + ? mNativeModuleCallExceptionHandler + : mDevSupportManager; + CatalystInstanceImpl.Builder catalystInstanceBuilder = new CatalystInstanceImpl.Builder() + .setReactQueueConfigurationSpec(ReactQueueConfigurationSpec.createDefault()) + .setJSExecutor(jsExecutor) + .setRegistry(nativeModuleRegistry) + .setJSModuleRegistry(jsModulesBuilder.build()) + .setJSBundleLoader(jsBundleLoader) + .setNativeModuleCallExceptionHandler(exceptionHandler); + + ReactMarker.logMarker(CREATE_CATALYST_INSTANCE_START); + // CREATE_CATALYST_INSTANCE_END is in JSCExecutor.cpp + Systrace.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "createCatalystInstance"); + final CatalystInstance catalystInstance; + try { + catalystInstance = catalystInstanceBuilder.build(); + } finally { + Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); + ReactMarker.logMarker(CREATE_CATALYST_INSTANCE_END); + } + + if (mBridgeIdleDebugListener != null) { + catalystInstance.addBridgeIdleDebugListener(mBridgeIdleDebugListener); + } + + reactContext.initializeWithInstance(catalystInstance); + + ReactMarker.logMarker(RUN_JS_BUNDLE_START); + catalystInstance.getReactQueueConfiguration().getJSQueueThread().runOnQueue(new Runnable() { + @Override + public void run() { + Systrace.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "runJSBundle"); + try { + catalystInstance.runJSBundle(); + } finally { + Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); + ReactMarker.logMarker(RUN_JS_BUNDLE_END); + } + } + }); + + return reactContext; + } + + private void processPackage( + ReactPackage reactPackage, + ReactApplicationContext reactContext, + NativeModuleRegistry.Builder nativeRegistryBuilder, + JavaScriptModuleRegistry.Builder jsModulesBuilder) { + for (NativeModule nativeModule : reactPackage.createNativeModules(reactContext)) { + nativeRegistryBuilder.add(nativeModule); + } + for (Class jsModuleClass : reactPackage.createJSModules()) { + jsModulesBuilder.add(jsModuleClass); + } + } + + private void moveReactContextToCurrentLifecycleState() { + if (mLifecycleState == LifecycleState.RESUMED) { + moveToResumedLifecycleState(true); + } + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/Arguments.java b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/Arguments.java new file mode 100644 index 000000000..fbfe0db11 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/Arguments.java @@ -0,0 +1,159 @@ +/** + * 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.cxxbridge; + +import java.lang.reflect.Array; + +import java.util.AbstractList; +import java.util.List; +import java.util.Map; + +import javax.annotation.Nullable; + +import android.os.Bundle; + +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.ReadableMapKeySetIterator; +import com.facebook.react.bridge.ReadableType; +import com.facebook.react.bridge.WritableNativeArray; +import com.facebook.react.bridge.WritableNativeMap; + +public class Arguments { + private static Object makeNativeObject(Object object) { + if (object == null) { + return null; + } else if (object instanceof Float || + object instanceof Long || + object instanceof Byte || + object instanceof Short) { + return new Double(((Number) object).doubleValue()); + } else if (object.getClass().isArray()) { + return makeNativeArray(object); + } else if (object instanceof List) { + return makeNativeArray((List) object); + } else if (object instanceof Map) { + return makeNativeMap((Map) object); + } else if (object instanceof Bundle) { + return makeNativeMap((Bundle) object); + } else { + // Boolean, Integer, Double, String, WritableNativeArray, WritableNativeMap + return object; + } + } + + /** + * 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. + */ + public static WritableNativeArray makeNativeArray(List objects) { + WritableNativeArray nativeArray = new WritableNativeArray(); + if (objects == null) { + return nativeArray; + } + for (Object elem : objects) { + elem = makeNativeObject(elem); + if (elem == null) { + nativeArray.pushNull(); + } else if (elem instanceof Boolean) { + nativeArray.pushBoolean((Boolean) elem); + } else if (elem instanceof Integer) { + nativeArray.pushInt((Integer) elem); + } else if (elem instanceof Double) { + nativeArray.pushDouble((Double) elem); + } else if (elem instanceof String) { + nativeArray.pushString((String) elem); + } else if (elem instanceof WritableNativeArray) { + nativeArray.pushArray((WritableNativeArray) elem); + } else if (elem instanceof WritableNativeMap) { + nativeArray.pushMap((WritableNativeMap) elem); + } else { + throw new IllegalArgumentException("Could not convert " + elem.getClass()); + } + } + return nativeArray; + } + + + /** + * This overload is like the above, but uses reflection to operate on any + * primitive or object type. + */ + public static WritableNativeArray makeNativeArray(final Object objects) { + if (objects == null) { + return new WritableNativeArray(); + } + // No explicit check for objects's type here. If it's not an array, the + // Array methods will throw IllegalArgumentException. + return makeNativeArray(new AbstractList() { + public int size() { + return Array.getLength(objects); + } + public Object get(int index) { + return Array.get(objects, index); + } + }); + } + + private static void addEntry(WritableNativeMap nativeMap, String key, Object value) { + value = makeNativeObject(value); + if (value == null) { + nativeMap.putNull(key); + } else if (value instanceof Boolean) { + nativeMap.putBoolean(key, (Boolean) value); + } else if (value instanceof Integer) { + nativeMap.putInt(key, (Integer) value); + } else if (value instanceof Number) { + nativeMap.putDouble(key, ((Number) value).doubleValue()); + } else if (value instanceof String) { + nativeMap.putString(key, (String) value); + } else if (value instanceof WritableNativeArray) { + nativeMap.putArray(key, (WritableNativeArray) value); + } else if (value instanceof WritableNativeMap) { + nativeMap.putMap(key, (WritableNativeMap) value); + } else { + throw new IllegalArgumentException("Could not convert " + value.getClass()); + } + } + + /** + * 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. + */ + public static WritableNativeMap makeNativeMap(Map objects) { + WritableNativeMap nativeMap = new WritableNativeMap(); + if (objects == null) { + return nativeMap; + } + for (Map.Entry entry : objects.entrySet()) { + addEntry(nativeMap, entry.getKey(), entry.getValue()); + } + return nativeMap; + } + + /** + * Like the above, but takes a Bundle instead of a Map. + */ + public static WritableNativeMap makeNativeMap(Bundle bundle) { + WritableNativeMap nativeMap = new WritableNativeMap(); + if (bundle == null) { + return nativeMap; + } + for (String key: bundle.keySet()) { + addEntry(nativeMap, key, bundle.get(key)); + } + return nativeMap; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/BUCK b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/BUCK new file mode 100644 index 000000000..feed57707 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/BUCK @@ -0,0 +1,38 @@ +include_defs('//ReactAndroid/DEFS') + +android_library( + name = 'bridge', + srcs = glob(['**/*.java']), + exported_deps = [ + react_native_dep('java/com/facebook/jni:jni'), + react_native_dep('java/com/facebook/proguard/annotations:annotations'), + ], + proguard_config = 'bridge.pro', + deps = [ + '//libraries/fbcore/src/main/java/com/facebook/common/logging:logging', + react_native_dep('java/com/facebook/systrace:systrace'), + react_native_dep('libraries/soloader/java/com/facebook/soloader:soloader'), + # TODO mhorowitz: + # java/com/facebook/catalyst/js/react-native-github/ReactAndroid/src/main/java/com/facebook/react/bridge/ + # lacks a similar dependency to this. This means that the + # loadLibrary calls in it are not guaranteed to succeed. This is + # kind of a mess for the jni/jni-internal stuff. In theory, we + # should be creating -internal android_library rules, too. In + # practice, since these are resolved at runtime, putting the + # dependency in the app works, too. gross. + # '//native/react/jni:jni-internal', + react_native_dep('third-party/java/infer-annotations:infer-annotations'), + react_native_dep('third-party/java/jackson:core'), + react_native_dep('third-party/java/jsr-305:jsr-305'), + react_native_dep('third-party/java/okhttp:okhttp3-ws'), + react_native_target('java/com/facebook/react/bridge:bridge'), + react_native_target('java/com/facebook/react/common:common'), + ], + visibility = [ + 'PUBLIC', + ], +) + +project_config( + src_target = ':bridge', +) diff --git a/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/CallbackImpl.java b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/CallbackImpl.java new file mode 100644 index 000000000..6c5f27ae3 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/CallbackImpl.java @@ -0,0 +1,30 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +package com.facebook.react.cxxbridge; + +import com.facebook.jni.HybridData; +import com.facebook.proguard.annotations.DoNotStrip; +import com.facebook.react.bridge.*; + +import static com.facebook.react.bridge.Arguments.*; + +/** + * Callback impl that calls directly into the cxxbridge. Created from C++. + */ +@DoNotStrip +public class CallbackImpl implements Callback { + @DoNotStrip + private final HybridData mHybridData; + + @DoNotStrip + private CallbackImpl(HybridData hybridData) { + mHybridData = hybridData; + } + + @Override + public void invoke(Object... args) { + nativeInvoke(fromJavaArgs(args)); + } + + private native void nativeInvoke(NativeArray arguments); +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/CatalystInstanceImpl.java b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/CatalystInstanceImpl.java new file mode 100644 index 000000000..3a5de280d --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/CatalystInstanceImpl.java @@ -0,0 +1,461 @@ +/** + * 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.cxxbridge; + +import javax.annotation.Nullable; + +import java.lang.ref.WeakReference; +import java.util.Collection; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicInteger; + +import android.content.res.AssetManager; + +import com.facebook.common.logging.FLog; +import com.facebook.jni.HybridData; +import com.facebook.react.bridge.CatalystInstance; +import com.facebook.react.bridge.ExecutorToken; +import com.facebook.react.bridge.JavaScriptModule; +import com.facebook.react.bridge.JavaScriptModuleRegistry; +import com.facebook.react.bridge.MemoryPressure; +import com.facebook.react.bridge.NativeArray; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.NativeModuleCallExceptionHandler; +import com.facebook.react.bridge.NotThreadSafeBridgeIdleDebugListener; +import com.facebook.react.bridge.queue.ReactQueueConfiguration; +import com.facebook.react.bridge.queue.MessageQueueThread; +import com.facebook.react.bridge.queue.QueueThreadExceptionHandler; +import com.facebook.react.bridge.queue.ReactQueueConfigurationImpl; +import com.facebook.react.bridge.queue.ReactQueueConfigurationSpec; +import com.facebook.proguard.annotations.DoNotStrip; +import com.facebook.react.common.ReactConstants; +import com.facebook.react.common.annotations.VisibleForTesting; +import com.facebook.infer.annotation.Assertions; +import com.facebook.soloader.SoLoader; +import com.facebook.systrace.Systrace; +import com.facebook.systrace.TraceListener; + +/** + * This provides an implementation of the public CatalystInstance instance. It is public because + * it is built by XReactInstanceManager which is in a different package. + */ +@DoNotStrip +public class CatalystInstanceImpl implements CatalystInstance { + + /* package */ static final String REACT_NATIVE_LIB = "reactnativejnifb"; + + static { + SoLoader.loadLibrary(REACT_NATIVE_LIB); + } + + private static final int BRIDGE_SETUP_TIMEOUT_MS = 30000; + private static final int LOAD_JS_BUNDLE_TIMEOUT_MS = 30000; + + private static final AtomicInteger sNextInstanceIdForTrace = new AtomicInteger(1); + + // Access from any thread + private final ReactQueueConfigurationImpl mReactQueueConfiguration; + private final CopyOnWriteArrayList 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 ExecutorToken mMainExecutorToken; + + private final NativeModuleRegistry mJavaRegistry; + private final NativeModuleCallExceptionHandler mNativeModuleCallExceptionHandler; + private boolean mInitialized = false; + + private boolean mJSBundleHasLoaded; + + // C++ parts + private final HybridData mHybridData; + private native static HybridData initHybrid(); + + private CatalystInstanceImpl( + final ReactQueueConfigurationSpec ReactQueueConfigurationSpec, + final JavaScriptExecutor jsExecutor, + final NativeModuleRegistry registry, + final JavaScriptModuleRegistry jsModuleRegistry, + final JSBundleLoader jsBundleLoader, + NativeModuleCallExceptionHandler nativeModuleCallExceptionHandler) { + FLog.d(ReactConstants.TAG, "Initializing React Xplat Bridge."); + mHybridData = initHybrid(); + + mReactQueueConfiguration = ReactQueueConfigurationImpl.create( + ReactQueueConfigurationSpec, + new NativeExceptionHandler()); + mBridgeIdleListeners = new CopyOnWriteArrayList<>(); + mJavaRegistry = registry; + mJSModuleRegistry = jsModuleRegistry; + mJSBundleLoader = jsBundleLoader; + mNativeModuleCallExceptionHandler = nativeModuleCallExceptionHandler; + mTraceListener = new JSProfilerTraceListener(this); + + initializeBridge( + new BridgeCallback(this), + jsExecutor, + mReactQueueConfiguration.getJSQueueThread(), + mReactQueueConfiguration.getNativeModulesQueueThread(), + mJavaRegistry.getModuleRegistryHolder(this)); + mMainExecutorToken = getMainExecutorToken(); + } + + private static class BridgeCallback implements ReactCallback { + // 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 mOuter; + + public BridgeCallback(CatalystInstanceImpl outer) { + mOuter = new WeakReference(outer); + } + + @Override + public void onBatchComplete() { + CatalystInstanceImpl impl = mOuter.get(); + if (impl != null) { + impl.mJavaRegistry.onBatchComplete(); + } + } + + @Override + public void incrementPendingJSCalls() { + CatalystInstanceImpl impl = mOuter.get(); + if (impl != null) { + impl.incrementPendingJSCalls(); + } + } + + @Override + public void decrementPendingJSCalls() { + CatalystInstanceImpl impl = mOuter.get(); + if (impl != null) { + impl.decrementPendingJSCalls(); + } + } + + @Override + public void onNativeException(Exception e) { + CatalystInstanceImpl impl = mOuter.get(); + if (impl != null) { + impl.onNativeException(e); + } + } + } + + private native void initializeBridge(ReactCallback callback, + JavaScriptExecutor jsExecutor, + MessageQueueThread jsQueue, + MessageQueueThread moduleQueue, + ModuleRegistryHolder registryHolder); + + /* package */ native void loadScriptFromAssets(AssetManager assetManager, String assetURL); + /* package */ native void loadScriptFromFile(String fileName, String sourceURL); + + @Override + public void runJSBundle() { + Assertions.assertCondition(!mJSBundleHasLoaded, "JS bundle was already loaded!"); + mJSBundleHasLoaded = true; + // incrementPendingJSCalls(); + mJSBundleLoader.loadScript(CatalystInstanceImpl.this); + // This is registered after JS starts since it makes a JS call + Systrace.registerListener(mTraceListener); + } + + private native void callJSFunction( + ExecutorToken token, + String module, + String method, + NativeArray arguments, + String tracingName); + + @Override + public void callFunction( + ExecutorToken executorToken, + final String module, + final String method, + final NativeArray arguments, + final String tracingName) { + if (mDestroyed) { + FLog.w(ReactConstants.TAG, "Calling JS function after bridge has been destroyed."); + return; + } + + callJSFunction(executorToken, module, method, arguments, tracingName); + } + + private native void callJSCallback(ExecutorToken executorToken, int callbackID, NativeArray arguments); + + @Override + public void invokeCallback(ExecutorToken executorToken, final int callbackID, final NativeArray arguments) { + if (mDestroyed) { + FLog.w(ReactConstants.TAG, "Invoking JS callback after bridge has been destroyed."); + return; + } + + callJSCallback(executorToken, callbackID, arguments); + } + + /** + * 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 + public void destroy() { + UiThreadUtil.assertOnUiThread(); + + if (mDestroyed) { + return; + } + + // TODO: tell all APIs to shut down + mDestroyed = true; + mHybridData.resetNative(); + mJavaRegistry.notifyCatalystInstanceDestroy(); + boolean wasIdle = (mPendingJSCalls.getAndSet(0) == 0); + if (!wasIdle && !mBridgeIdleListeners.isEmpty()) { + for (NotThreadSafeBridgeIdleDebugListener listener : mBridgeIdleListeners) { + listener.onTransitionToBridgeIdle(); + } + } + + // 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() { + UiThreadUtil.assertOnUiThread(); + Assertions.assertCondition( + !mInitialized, + "This catalyst instance has already been initialized"); + mInitialized = true; + mJavaRegistry.notifyCatalystInstanceInitialized(); + } + + @Override + public ReactQueueConfiguration getReactQueueConfiguration() { + return mReactQueueConfiguration; + } + + @Override + public T getJSModule(Class jsInterface) { + return getJSModule(mMainExecutorToken, jsInterface); + } + + @Override + public T getJSModule(ExecutorToken executorToken, Class jsInterface) { + return Assertions.assertNotNull(mJSModuleRegistry) + .getJavaScriptModule(this, executorToken, jsInterface); + } + + private native ExecutorToken getMainExecutorToken(); + + @Override + public boolean hasNativeModule(Class nativeModuleInterface) { + return mJavaRegistry.hasModule(nativeModuleInterface); + } + + // This is only ever called with UIManagerModule or CurrentViewerModule. + @Override + public T getNativeModule(Class nativeModuleInterface) { + return mJavaRegistry.getModule(nativeModuleInterface); + } + + // This is only used by com.facebook.react.modules.common.ModuleDataCleaner + @Override + public Collection getNativeModules() { + return mJavaRegistry.getAllModules(); + } + + @Override + public void handleMemoryPressure(MemoryPressure 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. + */ + @Override + public void addBridgeIdleDebugListener(NotThreadSafeBridgeIdleDebugListener listener) { + mBridgeIdleListeners.add(listener); + } + + /** + * Removes a NotThreadSafeBridgeIdleDebugListener previously added with + * {@link #addBridgeIdleDebugListener} + */ + @Override + public void removeBridgeIdleDebugListener(NotThreadSafeBridgeIdleDebugListener listener) { + mBridgeIdleListeners.remove(listener); + } + + @Override + public native void setGlobalVariable(String propName, String jsonValue); + + // TODO mhorowitz: add mDestroyed checks to the next three methods + + @Override + public native boolean supportsProfiling(); + + @Override + public native void startProfiler(String title); + + @Override + public native void stopProfiler(String title, String filename); + + private void incrementPendingJSCalls() { + int oldPendingCalls = mPendingJSCalls.getAndIncrement(); + boolean wasIdle = oldPendingCalls == 0; + Systrace.traceCounter( + Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, + mJsPendingCallsTitleForTrace, + oldPendingCalls + 1); + if (wasIdle && !mBridgeIdleListeners.isEmpty()) { + for (NotThreadSafeBridgeIdleDebugListener listener : mBridgeIdleListeners) { + listener.onTransitionToBridgeBusy(); + } + } + } + + 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( + Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, + mJsPendingCallsTitleForTrace, + newPendingCalls); + + if (isNowIdle && !mBridgeIdleListeners.isEmpty()) { + for (NotThreadSafeBridgeIdleDebugListener listener : mBridgeIdleListeners) { + listener.onTransitionToBridgeIdle(); + } + } + } + + private void onNativeException(Exception e) { + mNativeModuleCallExceptionHandler.handleException(e); + mReactQueueConfiguration.getUIQueueThread().runOnQueue( + new Runnable() { + @Override + public void run() { + 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 mOuter; + + public JSProfilerTraceListener(CatalystInstanceImpl outer) { + mOuter = new WeakReference(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 JavaScriptModuleRegistry mJSModuleRegistry; + private @Nullable JavaScriptExecutor mJSExecutor; + private @Nullable NativeModuleCallExceptionHandler mNativeModuleCallExceptionHandler; + + public Builder setReactQueueConfigurationSpec( + ReactQueueConfigurationSpec ReactQueueConfigurationSpec) { + mReactQueueConfigurationSpec = ReactQueueConfigurationSpec; + return this; + } + + public Builder setRegistry(NativeModuleRegistry registry) { + mRegistry = registry; + return this; + } + + public Builder setJSModuleRegistry(JavaScriptModuleRegistry jsModuleRegistry) { + mJSModuleRegistry = jsModuleRegistry; + return this; + } + + public Builder setJSBundleLoader(JSBundleLoader jsBundleLoader) { + mJSBundleLoader = jsBundleLoader; + return this; + } + + public Builder setJSExecutor(JavaScriptExecutor jsExecutor) { + mJSExecutor = jsExecutor; + return this; + } + + public Builder setNativeModuleCallExceptionHandler( + NativeModuleCallExceptionHandler handler) { + mNativeModuleCallExceptionHandler = handler; + return this; + } + + public CatalystInstanceImpl build() { + return new CatalystInstanceImpl( + Assertions.assertNotNull(mReactQueueConfigurationSpec), + Assertions.assertNotNull(mJSExecutor), + Assertions.assertNotNull(mRegistry), + Assertions.assertNotNull(mJSModuleRegistry), + Assertions.assertNotNull(mJSBundleLoader), + Assertions.assertNotNull(mNativeModuleCallExceptionHandler)); + } + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/CxxModuleWrapper.java b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/CxxModuleWrapper.java new file mode 100644 index 000000000..14d8a7dfd --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/CxxModuleWrapper.java @@ -0,0 +1,110 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +package com.facebook.react.cxxbridge; + +import com.facebook.react.bridge.BaseJavaModule; +import com.facebook.react.bridge.CatalystInstance; +import com.facebook.react.bridge.ExecutorToken; +import com.facebook.react.bridge.JsonWriter; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.ReactBridge; +import com.facebook.react.bridge.ReadableNativeArray; + +import com.facebook.jni.HybridData; +import com.facebook.proguard.annotations.DoNotStrip; +import com.facebook.soloader.SoLoader; + +import java.io.IOException; +import java.util.Map; + +/** + * A Java Object which represents a cross-platform C++ module + * + */ +@DoNotStrip +public class CxxModuleWrapper implements NativeModule +{ + static { + SoLoader.loadLibrary(CatalystInstanceImpl.REACT_NATIVE_LIB); + } + + @DoNotStrip + private HybridData mHybridData; + + @DoNotStrip + private static class MethodWrapper implements NativeMethod + { + @DoNotStrip + HybridData mHybridData; + + MethodWrapper() { + mHybridData = initHybrid(); + } + + public native HybridData initHybrid(); + + @Override + public native void invoke(CatalystInstance catalystInstance, ExecutorToken executorToken, ReadableNativeArray args); + + @Override + public String getType() { + return BaseJavaModule.METHOD_TYPE_REMOTE; + } + } + + public CxxModuleWrapper(String library, String factory) { + SoLoader.loadLibrary(library); + mHybridData = + initHybrid(SoLoader.unpackLibraryAndDependencies(library).getAbsolutePath(), factory); + } + + @Override + public native String getName(); + + @Override + public native Map getMethods(); + + @Override + public void writeConstantsField(JsonWriter writer, String fieldName) throws IOException { + String constants = getConstantsJson(); + if (constants == null || constants.isEmpty()) { + return; + } + + writer.name(fieldName).rawValue(constants); + } + + public native String getConstantsJson(); + + @Override + public void initialize() { + // do nothing + } + + @Override + public boolean canOverrideExistingModule() { + return false; + } + + @Override + public boolean supportsWebWorkers() { + return false; + } + + @Override + public void onReactBridgeInitialized(ReactBridge bridge) { + // do nothing + } + + @Override + public void onCatalystInstanceDestroy() { + mHybridData.resetNative(); + } + + // For creating a wrapper from C++, or from a derived class. + protected CxxModuleWrapper(HybridData hd) { + mHybridData = hd; + } + + private native HybridData initHybrid(String soPath, String factory); +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/ExecutorToken.java b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/ExecutorToken.java new file mode 100644 index 000000000..e69de29bb diff --git a/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/JSBundleLoader.java b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/JSBundleLoader.java new file mode 100644 index 000000000..09fd6a4a6 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/JSBundleLoader.java @@ -0,0 +1,90 @@ +/** + * 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.cxxbridge; + +import android.content.Context; + +/** + * A class that stores JS bundle information and allows {@link CatalystInstance} to load a correct + * bundle through {@link ReactBridge}. + */ +public abstract class JSBundleLoader { + + /** + * 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 directory in native code to save on passing + * large strings from java to native memory. + */ + public static JSBundleLoader createFileLoader( + final Context context, + final String fileName) { + return new JSBundleLoader() { + @Override + public void loadScript(CatalystInstanceImpl instance) { + if (fileName.startsWith("assets://")) { + instance.loadScriptFromAssets(context.getAssets(), fileName); + } else { + instance.loadScriptFromFile(fileName, fileName); + } + } + + @Override + public String getSourceUrl() { + 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 {@param sourceURL} of downloaded bundle is required for JS stacktraces to + * work correctly and allows for source maps to correctly symbolize those. + */ + public static JSBundleLoader createCachedBundleFromNetworkLoader( + final String sourceURL, + final String cachedFileLocation) { + return new JSBundleLoader() { + @Override + public void loadScript(CatalystInstanceImpl instance) { + instance.loadScriptFromFile(cachedFileLocation, sourceURL); + } + + @Override + public String getSourceUrl() { + return sourceURL; + } + }; + } + + /** + * This loader is used when proxy debugging is enabled. In that case there is no point in fetching + * the bundle from device as remote executor will have to do it anyway. + */ + public static JSBundleLoader createRemoteDebuggerBundleLoader( + final String proxySourceURL, + final String realSourceURL) { + return new JSBundleLoader() { + @Override + public void loadScript(CatalystInstanceImpl instance) { + instance.loadScriptFromFile(null, proxySourceURL); + } + + @Override + public String getSourceUrl() { + return realSourceURL; + } + }; + } + + public abstract void loadScript(CatalystInstanceImpl instance); + public abstract String getSourceUrl(); +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/JSCJavaScriptExecutor.java b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/JSCJavaScriptExecutor.java new file mode 100644 index 000000000..36e0bb29e --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/JSCJavaScriptExecutor.java @@ -0,0 +1,46 @@ +/** + * 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.cxxbridge; + +import com.facebook.jni.HybridData; +import com.facebook.proguard.annotations.DoNotStrip; +import com.facebook.react.bridge.ReadableNativeArray; +import com.facebook.react.bridge.WritableNativeArray; +import com.facebook.react.bridge.WritableNativeMap; +import com.facebook.soloader.SoLoader; + +@DoNotStrip +public class JSCJavaScriptExecutor extends JavaScriptExecutor { + public static class Factory implements JavaScriptExecutor.Factory { + private ReadableNativeArray mJSCConfig; + + public Factory(WritableNativeMap jscConfig) { + // TODO (t10707444): use NativeMap, which requires moving NativeMap out of OnLoad. + WritableNativeArray array = new WritableNativeArray(); + array.pushMap(jscConfig); + mJSCConfig = array; + } + + @Override + public JavaScriptExecutor create() throws Exception { + return new JSCJavaScriptExecutor(mJSCConfig); + } + } + + static { + SoLoader.loadLibrary(CatalystInstanceImpl.REACT_NATIVE_LIB); + } + + public JSCJavaScriptExecutor(ReadableNativeArray jscConfig) { + super(initHybrid(jscConfig)); + } + + private native static HybridData initHybrid(ReadableNativeArray jscConfig); +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/JavaModuleWrapper.java b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/JavaModuleWrapper.java new file mode 100644 index 000000000..f9dc5cbb5 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/JavaModuleWrapper.java @@ -0,0 +1,141 @@ +/** + * 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.cxxbridge; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import com.facebook.proguard.annotations.DoNotStrip; + +import com.facebook.react.bridge.BaseJavaModule; +import com.facebook.react.bridge.CatalystInstance; +import com.facebook.react.bridge.ExecutorToken; +import com.facebook.react.bridge.NativeArray; +import com.facebook.react.bridge.ReadableNativeArray; +import com.facebook.react.bridge.WritableNativeArray; + +/** + * This is part of the glue which wraps a java BaseJavaModule in a C++ + * NativeModule. This could all be in C++, but it's android-specific + * initialization code, and writing it this way is easier to read and means + * fewer JNI calls. + */ + +@DoNotStrip +/* package */ class JavaModuleWrapper { + @DoNotStrip + public class MethodDescriptor { + @DoNotStrip + Method method; + @DoNotStrip + String signature; + @DoNotStrip + String name; + @DoNotStrip + String type; + } + + private final CatalystInstance mCatalystInstance; + private final BaseJavaModule mModule; + private final ArrayList mMethods; + + public JavaModuleWrapper(CatalystInstance catalystinstance, BaseJavaModule module) { + mCatalystInstance = catalystinstance; + mModule = module; + mMethods = new ArrayList(); + } + + @DoNotStrip + public BaseJavaModule getModule() { + return mModule; + } + + @DoNotStrip + public String getName() { + return mModule.getName(); + } + + @DoNotStrip + public List getMethodDescriptors() { + ArrayList descs = new ArrayList<>(); + + for (Map.Entry entry : + mModule.getMethods().entrySet()) { + MethodDescriptor md = new MethodDescriptor(); + md.name = entry.getKey(); + md.type = entry.getValue().getType(); + + BaseJavaModule.JavaMethod method = (BaseJavaModule.JavaMethod) entry.getValue(); + mMethods.add(method); + + descs.add(md); + } + + return descs; + } + + @DoNotStrip + public List newGetMethodDescriptors() { + ArrayList descs = new ArrayList<>(); + + for (Map.Entry entry : + mModule.getMethods().entrySet()) { + MethodDescriptor md = new MethodDescriptor(); + md.name = entry.getKey(); + md.type = entry.getValue().getType(); + + BaseJavaModule.JavaMethod method = (BaseJavaModule.JavaMethod) entry.getValue(); + md.method = method.getMethod(); + md.signature = method.getSignature(); + + descs.add(md); + } + + for (Map.Entry entry : + mModule.getSyncHooks().entrySet()) { + MethodDescriptor md = new MethodDescriptor(); + md.name = entry.getKey(); + md.type = BaseJavaModule.METHOD_TYPE_SYNC_HOOK; + + BaseJavaModule.SyncJavaHook method = (BaseJavaModule.SyncJavaHook) entry.getValue(); + md.method = method.getMethod(); + md.signature = method.getSignature(); + + descs.add(md); + } + + return descs; + } + + // TODO mhorowitz: make this return NativeMap, which requires moving + // NativeMap out of OnLoad. + @DoNotStrip + public NativeArray getConstants() { + WritableNativeArray array = new WritableNativeArray(); + array.pushMap(Arguments.makeNativeMap(mModule.getConstants())); + return array; + } + + @DoNotStrip + public boolean supportsWebWorkers() { + return mModule.supportsWebWorkers(); + } + + @DoNotStrip + public void invoke(ExecutorToken token, int methodId, ReadableNativeArray parameters) { + if (mMethods == null || methodId >= mMethods.size()) { + return; + } + + mMethods.get(methodId).invoke(mCatalystInstance, token, parameters); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/JavaScriptExecutor.java b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/JavaScriptExecutor.java new file mode 100644 index 000000000..7f1573240 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/JavaScriptExecutor.java @@ -0,0 +1,35 @@ +/** + * 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.cxxbridge; + +import com.facebook.jni.HybridData; +import com.facebook.proguard.annotations.DoNotStrip; + +@DoNotStrip +public abstract class JavaScriptExecutor { + public interface Factory { + JavaScriptExecutor create() throws Exception; + } + + private final HybridData mHybridData; + + protected JavaScriptExecutor(HybridData hybridData) { + mHybridData = hybridData; + } + + /** + * Close this executor and cleanup any resources that it was using. No further calls are + * expected after this. + * TODO mhorowitz: This may no longer be used; check and delete if possible. + */ + public void close() { + mHybridData.resetNative(); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/ModuleRegistryHolder.java b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/ModuleRegistryHolder.java new file mode 100644 index 000000000..da271d27a --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/ModuleRegistryHolder.java @@ -0,0 +1,28 @@ +/** + * 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.cxxbridge; + +import java.util.Collection; + +import com.facebook.jni.HybridData; + +public class ModuleRegistryHolder { + private final HybridData mHybridData; + private static native HybridData initHybrid( + CatalystInstanceImpl catalystInstanceImpl, + Collection javaModules, + Collection cxxModules); + + public ModuleRegistryHolder(CatalystInstanceImpl catalystInstanceImpl, + Collection javaModules, + Collection cxxModules) { + mHybridData = initHybrid(catalystInstanceImpl, javaModules, cxxModules); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/NativeModuleRegistry.java b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/NativeModuleRegistry.java new file mode 100644 index 000000000..c8030aa81 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/NativeModuleRegistry.java @@ -0,0 +1,138 @@ +/** + * 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.cxxbridge; + +import java.io.IOException; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import javax.annotation.Nullable; + +import com.facebook.react.bridge.BaseJavaModule; +import com.facebook.react.bridge.CatalystInstance; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.OnBatchCompleteListener; +import com.facebook.react.bridge.ReadableNativeArray; +import com.facebook.react.common.MapBuilder; +import com.facebook.react.common.SetBuilder; +import com.facebook.infer.annotation.Assertions; +import com.facebook.systrace.Systrace; + +/** + * A set of Java APIs to expose to a particular JavaScript instance. + */ +public class NativeModuleRegistry { + private final Map, NativeModule> mModuleInstances; + private final ArrayList mBatchCompleteListenerModules; + + private NativeModuleRegistry(Map, NativeModule> moduleInstances) { + mModuleInstances = moduleInstances; + mBatchCompleteListenerModules = new ArrayList(mModuleInstances.size()); + for (NativeModule module : mModuleInstances.values()) { + if (module instanceof OnBatchCompleteListener) { + mBatchCompleteListenerModules.add((OnBatchCompleteListener) module); + } + } + } + + /* package */ ModuleRegistryHolder getModuleRegistryHolder( + CatalystInstanceImpl catalystInstanceImpl) { + ArrayList javaModules = new ArrayList<>(); + ArrayList cxxModules = new ArrayList<>(); + for (NativeModule module : mModuleInstances.values()) { + if (module instanceof BaseJavaModule) { + javaModules.add(new JavaModuleWrapper(catalystInstanceImpl, (BaseJavaModule) module)); + } else if (module instanceof CxxModuleWrapper) { + cxxModules.add((CxxModuleWrapper) module); + } else { + throw new IllegalArgumentException("Unknown module type " + module.getClass()); + } + } + return new ModuleRegistryHolder(catalystInstanceImpl, javaModules, cxxModules); + } + + /* package */ void notifyCatalystInstanceDestroy() { + UiThreadUtil.assertOnUiThread(); + Systrace.beginSection( + Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, + "NativeModuleRegistry_notifyCatalystInstanceDestroy"); + try { + for (NativeModule nativeModule : mModuleInstances.values()) { + nativeModule.onCatalystInstanceDestroy(); + } + } finally { + Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); + } + } + + /* package */ void notifyCatalystInstanceInitialized() { + UiThreadUtil.assertOnUiThread(); + + ReactMarker.logMarker("NativeModule_start"); + Systrace.beginSection( + Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, + "NativeModuleRegistry_notifyCatalystInstanceInitialized"); + try { + for (NativeModule nativeModule : mModuleInstances.values()) { + nativeModule.initialize(); + } + } finally { + Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); + ReactMarker.logMarker("NativeModule_end"); + } + } + + public void onBatchComplete() { + for (int i = 0; i < mBatchCompleteListenerModules.size(); i++) { + mBatchCompleteListenerModules.get(i).onBatchComplete(); + } + } + + public boolean hasModule(Class moduleInterface) { + return mModuleInstances.containsKey(moduleInterface); + } + + public T getModule(Class moduleInterface) { + return (T) Assertions.assertNotNull(mModuleInstances.get(moduleInterface)); + } + + public Collection getAllModules() { + return mModuleInstances.values(); + } + + public static class Builder { + private final HashMap mModules = MapBuilder.newHashMap(); + + public Builder add(NativeModule module) { + NativeModule existing = mModules.get(module.getName()); + if (existing != null && !module.canOverrideExistingModule()) { + throw new IllegalStateException("Native module " + module.getClass().getSimpleName() + + " tried to override " + existing.getClass().getSimpleName() + " for module name " + + module.getName() + ". If this was your intention, return true from " + + module.getClass().getSimpleName() + "#canOverrideExistingModule()"); + } + mModules.put(module.getName(), module); + return this; + } + + public NativeModuleRegistry build() { + Map, NativeModule> moduleInstances = new HashMap<>(); + for (NativeModule module : mModules.values()) { + moduleInstances.put((Class)module.getClass(), module); + } + return new NativeModuleRegistry(moduleInstances); + } + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/ProxyJavaScriptExecutor.java b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/ProxyJavaScriptExecutor.java new file mode 100644 index 000000000..48fd281e3 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/ProxyJavaScriptExecutor.java @@ -0,0 +1,67 @@ +/** + * 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.cxxbridge; + +import javax.annotation.Nullable; + +import com.facebook.jni.HybridData; +import com.facebook.soloader.SoLoader; +import com.facebook.proguard.annotations.DoNotStrip; +import com.facebook.react.bridge.JavaJSExecutor; + +/** + * JavaScript executor that delegates JS calls processed by native code back to a java version + * of the native executor interface. + * + * When set as a executor with {@link CatalystInstance.Builder}, catalyst native code will delegate + * low level javascript calls to the implementation of {@link JavaJSExecutor} interface provided + * with the constructor of this class. + */ +@DoNotStrip +public class ProxyJavaScriptExecutor extends JavaScriptExecutor { + public static class Factory implements JavaScriptExecutor.Factory { + private final JavaJSExecutor.Factory mJavaJSExecutorFactory; + + public Factory(JavaJSExecutor.Factory javaJSExecutorFactory) { + mJavaJSExecutorFactory = javaJSExecutorFactory; + } + + @Override + public JavaScriptExecutor create() throws Exception { + return new ProxyJavaScriptExecutor(mJavaJSExecutorFactory.create()); + } + } + + static { + SoLoader.loadLibrary(CatalystInstanceImpl.REACT_NATIVE_LIB); + } + + private @Nullable JavaJSExecutor mJavaJSExecutor; + + /** + * Create {@link ProxyJavaScriptExecutor} instance + * @param executor implementation of {@link JavaJSExecutor} which will be responsible for handling + * javascript calls + */ + public ProxyJavaScriptExecutor(JavaJSExecutor executor) { + super(initHybrid(executor)); + mJavaJSExecutor = executor; + } + + @Override + public void close() { + if (mJavaJSExecutor != null) { + mJavaJSExecutor.close(); + mJavaJSExecutor = null; + } + } + + private native static HybridData initHybrid(JavaJSExecutor executor); +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/ReactCallback.java b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/ReactCallback.java new file mode 100644 index 000000000..6eb743dbe --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/ReactCallback.java @@ -0,0 +1,27 @@ +/** + * 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.cxxbridge; + +import com.facebook.proguard.annotations.DoNotStrip; + +@DoNotStrip +/* package */ interface ReactCallback { + @DoNotStrip + void onBatchComplete(); + + @DoNotStrip + void incrementPendingJSCalls(); + + @DoNotStrip + void decrementPendingJSCalls(); + + @DoNotStrip + void onNativeException(Exception e); +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/ReactMarker.java b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/ReactMarker.java new file mode 100644 index 000000000..3d742f736 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/ReactMarker.java @@ -0,0 +1,31 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +package com.facebook.react.cxxbridge; + +import javax.annotation.Nullable; +import com.facebook.proguard.annotations.DoNotStrip; +/** + * Static class that allows markers to be placed in React code and responded to in a + * configurable way + */ +@DoNotStrip +public class ReactMarker { + + public interface MarkerListener { + void logMarker(String name); + }; + + @Nullable static private MarkerListener sMarkerListener = null; + + static public void setMarkerListener(MarkerListener listener) { + sMarkerListener = listener; + } + + @DoNotStrip + static public void logMarker(String name) { + if (sMarkerListener != null) { + sMarkerListener.logMarker(name); + } + } + +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/SoftAssertions.java b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/SoftAssertions.java new file mode 100644 index 000000000..8809540a0 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/SoftAssertions.java @@ -0,0 +1,52 @@ +/** + * 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.cxxbridge; + +import javax.annotation.Nullable; + +import com.facebook.react.bridge.AssertionException; + +/** + * Utility class to make assertions that should not hard-crash the app but instead be handled by the + * Catalyst app {@link NativeModuleCallExceptionHandler}. See the javadoc on that class for + * more information about our opinion on when these assertions should be used as opposed to + * assertions that might throw AssertionError Throwables that will cause the app to hard crash. + */ +public class SoftAssertions { + + /** + * Throw {@link AssertionException} with a given message. Use this method surrounded with + * {@code if} block with assert condition in case you plan to do string concatenation to produce + * the message. + */ + public static void assertUnreachable(String message) { + throw new AssertionException(message); + } + + /** + * Asserts the given condition, throwing an {@link AssertionException} if the condition doesn't + * hold. + */ + public static void assertCondition(boolean condition, String message) { + if (!condition) { + throw new AssertionException(message); + } + } + + /** + * Asserts that the given Object isn't null, throwing an {@link AssertionException} if it was. + */ + public static T assertNotNull(@Nullable T instance) { + if (instance == null) { + throw new AssertionException("Expected object to not be null!"); + } + return instance; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/UiThreadUtil.java b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/UiThreadUtil.java new file mode 100644 index 000000000..d8624474b --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/UiThreadUtil.java @@ -0,0 +1,56 @@ +/** + * 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.cxxbridge; + +import javax.annotation.Nullable; + +import android.os.Handler; +import android.os.Looper; + +/** + * Utility for interacting with the UI thread. + */ +public class UiThreadUtil { + + @Nullable private static Handler sMainHandler; + + /** + * @return {@code true} if the current thread is the UI thread. + */ + public static boolean isOnUiThread() { + return Looper.getMainLooper().getThread() == Thread.currentThread(); + } + + /** + * Throws an {@link AssertionException} if the current thread is not the UI thread. + */ + public static void assertOnUiThread() { + SoftAssertions.assertCondition(isOnUiThread(), "Expected to run on UI thread!"); + } + + /** + * Throws an {@link AssertionException} if the current thread is the UI thread. + */ + public static void assertNotOnUiThread() { + SoftAssertions.assertCondition(!isOnUiThread(), "Expected not to run on UI thread!"); + } + + /** + * Runs the given {@code Runnable} on the UI thread. + */ + public static void runOnUiThread(Runnable runnable) { + synchronized (UiThreadUtil.class) { + if (sMainHandler == null) { + sMainHandler = new Handler(Looper.getMainLooper()); + } + } + sMainHandler.post(runnable); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/bridge.pro b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/bridge.pro new file mode 100644 index 000000000..a03ce0287 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/bridge.pro @@ -0,0 +1,7 @@ +## Putting this here is kind of a hack. I don't want to modify the OSS bridge. +## TODO mhorowitz: add @DoNotStrip to the interface directly. + +-keepclassmembers class com.facebook.react.bridge.queue.MessageQueueThread { + public boolean isOnThread(); + public void assertIsOnThread(); +}