diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java b/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java index ba4cd8d12..609b320fc 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java @@ -16,44 +16,16 @@ import java.util.List; 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.Arguments; -import com.facebook.react.bridge.CatalystInstance; -import com.facebook.react.bridge.JSBundleLoader; -import com.facebook.react.bridge.JSCJavaScriptExecutor; -import com.facebook.react.bridge.JavaJSExecutor; -import com.facebook.react.bridge.JavaScriptExecutor; -import com.facebook.react.bridge.JavaScriptModule; -import com.facebook.react.bridge.JavaScriptModulesConfig; -import com.facebook.react.bridge.NativeModule; -import com.facebook.react.bridge.NativeModuleRegistry; import com.facebook.react.bridge.NotThreadSafeBridgeIdleDebugListener; -import com.facebook.react.bridge.ProxyJavaScriptExecutor; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContext; -import com.facebook.react.bridge.UiThreadUtil; -import com.facebook.react.bridge.WritableMap; -import com.facebook.react.bridge.WritableNativeMap; -import com.facebook.react.bridge.queue.CatalystQueueConfigurationSpec; -import com.facebook.react.common.ReactConstants; import com.facebook.react.common.annotations.VisibleForTesting; -import com.facebook.react.devsupport.DevServerHelper; import com.facebook.react.devsupport.DevSupportManager; -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.ReactNative; -import com.facebook.react.uimanager.UIManagerModule; import com.facebook.react.uimanager.ViewManager; -import com.facebook.soloader.SoLoader; /** * This class is managing instances of {@link CatalystInstance}. It expose a way to configure @@ -70,169 +42,11 @@ import com.facebook.soloader.SoLoader; * owning activity's lifecycle events to the instance manager (see {@link #onPause}, * {@link #onDestroy} and {@link #onResume}). * - * To instantiate an instance of this class use {@link #builder}. + * Ideally, this would be an interface, but because of the API used by earlier versions, it has to + * have a static method, and so cannot (in Java < 8), be one. */ -public class ReactInstanceManager { - - /* should only be accessed from main thread (UI thread) */ - private final List mAttachedRootViews = new ArrayList<>(); - private LifecycleState mLifecycleState; - private boolean mIsContextInitAsyncTaskRunning; - private @Nullable ReactContextInitParams mPendingReactContextInitParams; - - /* 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 volatile boolean mHasStartedCreatingInitialContext = false; - - private final ReactInstanceDevCommandsHandler mDevInterface = - new ReactInstanceDevCommandsHandler() { - - @Override - public void onReloadWithJSDebugger(JavaJSExecutor jsExecutor) { - ReactInstanceManager.this.onReloadWithJSDebugger(jsExecutor); - } - - @Override - public void onJSBundleLoadedFromServer() { - ReactInstanceManager.this.onJSBundleLoadedFromServer(); - } - - @Override - public void toggleElementInspector() { - ReactInstanceManager.this.toggleElementInspector(); - } - }; - - private final DefaultHardwareBackBtnHandler mBackBtnHandler = - new DefaultHardwareBackBtnHandler() { - @Override - public void invokeDefaultOnBackPressed() { - ReactInstanceManager.this.invokeDefaultOnBackPressed(); - } - }; - - private class ReactContextInitParams { - private final JavaScriptExecutor mJsExecutor; - private final JSBundleLoader mJsBundleLoader; - - public ReactContextInitParams( - JavaScriptExecutor jsExecutor, - JSBundleLoader jsBundleLoader) { - mJsExecutor = Assertions.assertNotNull(jsExecutor); - mJsBundleLoader = Assertions.assertNotNull(jsBundleLoader); - } - - public JavaScriptExecutor getJsExecutor() { - return mJsExecutor; - } - - 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 ReactApplicationContext doInBackground(ReactContextInitParams... params) { - Assertions.assertCondition(params != null && params.length > 0 && params[0] != null); - return createReactContext(params[0].getJsExecutor(), params[0].getJsBundleLoader()); - } - - @Override - protected void onPostExecute(ReactApplicationContext reactContext) { - try { - setupReactContext(reactContext); - } finally { - mIsContextInitAsyncTaskRunning = false; - } - - // Handle enqueued request to re-initialize react context. - if (mPendingReactContextInitParams != null) { - recreateReactContextInBackground( - mPendingReactContextInitParams.getJsExecutor(), - mPendingReactContextInitParams.getJsBundleLoader()); - mPendingReactContextInitParams = null; - } - } - } - - private ReactInstanceManager( - Context applicationContext, - @Nullable String jsBundleFile, - @Nullable String jsMainModuleName, - List packages, - boolean useDeveloperSupport, - @Nullable NotThreadSafeBridgeIdleDebugListener bridgeIdleDebugListener, - LifecycleState initialLifecycleState) { - initializeSoLoaderIfNecessary(applicationContext); - - mApplicationContext = applicationContext; - mJSBundleFile = jsBundleFile; - mJSMainModuleName = jsMainModuleName; - mPackages = packages; - mUseDeveloperSupport = useDeveloperSupport; - // We need to instantiate DevSupportManager regardless to the useDeveloperSupport option, - // although will prevent dev support manager from displaying any options or dialogs by - // checking useDeveloperSupport option before calling setDevSupportEnabled on this manager - // TODO(6803830): Don't instantiate devsupport manager when useDeveloperSupport is false - mDevSupportManager = new DevSupportManager( - applicationContext, - mDevInterface, - mJSMainModuleName, - useDeveloperSupport); - mBridgeIdleDebugListener = bridgeIdleDebugListener; - mLifecycleState = initialLifecycleState; - } - - public DevSupportManager getDevSupportManager() { - return mDevSupportManager; - } - - /** - * Creates a builder that is capable of creating an instance of {@link ReactInstanceManager}. - */ - public static Builder builder() { - return new Builder(); - } - - 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 ReactInstanceManager - SoLoader.init(applicationContext, /* native exopackage */ false); - } - - public void setJSBundleFile(String jsBundleFile) { - mJSBundleFile = jsBundleFile; - } +public abstract class ReactInstanceManager { + public abstract DevSupportManager getDevSupportManager(); /** * Trigger react context initialization asynchronously in a background async task. This enables @@ -243,129 +57,20 @@ public class ReactInstanceManager { * * Called from UI thread. */ - 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(), - JSBundleLoader.createFileLoader(mApplicationContext, mJSBundleFile)); - } + public abstract void createReactContextInBackground(); /** * @return whether createReactContextInBackground has been called. Will return false after * onDestroy until a new initial context has been created. */ - public boolean hasStartedCreatingInitialContext() { - return mHasStartedCreatingInitialContext; - } + public abstract boolean hasStartedCreatingInitialContext(); /** * 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. */ - 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); - } - } - - public void onPause() { - UiThreadUtil.assertOnUiThread(); - - mLifecycleState = LifecycleState.BEFORE_RESUME; - - mDefaultBackButtonImpl = null; - if (mUseDeveloperSupport) { - mDevSupportManager.setDevSupportEnabled(false); - } - - mCurrentActivity = null; - if (mCurrentReactContext != null) { - mCurrentReactContext.onPause(); - } - } - + public abstract void onBackPressed(); + public abstract void onPause(); /** * Use this method when the activity resumes to enable invoking the back button directly from JS. * @@ -377,53 +82,17 @@ public class ReactInstanceManager { * @param defaultBackButtonImpl a {@link DefaultHardwareBackBtnHandler} from an Activity that owns * this instance of {@link ReactInstanceManager}. */ - public void onResume(Activity activity, DefaultHardwareBackBtnHandler defaultBackButtonImpl) { - UiThreadUtil.assertOnUiThread(); - - mLifecycleState = LifecycleState.RESUMED; - - mDefaultBackButtonImpl = defaultBackButtonImpl; - if (mUseDeveloperSupport) { - mDevSupportManager.setDevSupportEnabled(true); - } - - mCurrentActivity = activity; - if (mCurrentReactContext != null) { - mCurrentReactContext.onResume(activity); - } - } - - public void onDestroy() { - UiThreadUtil.assertOnUiThread(); - - if (mUseDeveloperSupport) { - mDevSupportManager.setDevSupportEnabled(false); - } - - if (mCurrentReactContext != null) { - mCurrentReactContext.onDestroy(); - mCurrentReactContext = null; - mHasStartedCreatingInitialContext = false; - } - } - - public void onActivityResult(int requestCode, int resultCode, Intent data) { - if (mCurrentReactContext != null) { - mCurrentReactContext.onActivityResult(requestCode, resultCode, data); - } - } - - public void showDevOptionsDialog() { - UiThreadUtil.assertOnUiThread(); - mDevSupportManager.showDevOptionsDialog(); - } + public abstract void onResume( + Activity activity, + DefaultHardwareBackBtnHandler defaultBackButtonImpl); + public abstract void onDestroy(); + public abstract void onActivityResult(int requestCode, int resultCode, Intent data); + public abstract void showDevOptionsDialog(); /** * Get the URL where the last bundle was loaded from. */ - public String getSourceUrl() { - return Assertions.assertNotNull(mSourceUrl); - } + public abstract String getSourceUrl(); /** * Attach given {@param rootView} to a catalyst instance manager and start JS application using @@ -433,218 +102,46 @@ public class ReactInstanceManager { * This view will then be tracked by this manager and in case of catalyst instance restart it will * be re-attached. */ - /* package */ 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 (!mIsContextInitAsyncTaskRunning && mCurrentReactContext != null) { - attachMeasuredRootViewToInstance(rootView, mCurrentReactContext.getCatalystInstance()); - } - } + public abstract void attachMeasuredRootView(ReactRootView rootView); /** * 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. */ - /* package */ void detachRootView(ReactRootView rootView) { - UiThreadUtil.assertOnUiThread(); - if (mAttachedRootViews.remove(rootView)) { - if (mCurrentReactContext != null && mCurrentReactContext.hasActiveCatalystInstance()) { - detachViewFromInstance(rootView, mCurrentReactContext.getCatalystInstance()); - } - } - } + public abstract void detachRootView(ReactRootView rootView); /** * Uses configured {@link ReactPackage} instances to create all view managers */ - /* package */ List createAllViewManagers( - ReactApplicationContext catalystApplicationContext) { - List allViewManagers = new ArrayList<>(); - for (ReactPackage reactPackage : mPackages) { - allViewManagers.addAll(reactPackage.createViewManagers(catalystApplicationContext)); - } - return allViewManagers; - } + public abstract List createAllViewManagers( + ReactApplicationContext catalystApplicationContext); @VisibleForTesting - public @Nullable ReactContext getCurrentReactContext() { - return mCurrentReactContext; - } - - private void onReloadWithJSDebugger(JavaJSExecutor jsExecutor) { - recreateReactContextInBackground( - new ProxyJavaScriptExecutor(jsExecutor), - JSBundleLoader.createRemoteDebuggerBundleLoader( - mDevSupportManager.getJSBundleURLForRemoteDebugging())); - } - - private void onJSBundleLoadedFromServer() { - recreateReactContextInBackground( - new JSCJavaScriptExecutor(), - JSBundleLoader.createCachedBundleFromNetworkLoader( - mDevSupportManager.getSourceUrl(), - mDevSupportManager.getDownloadedJSBundleFile())); - } - - private void recreateReactContextInBackground( - JavaScriptExecutor jsExecutor, - JSBundleLoader jsBundleLoader) { - UiThreadUtil.assertOnUiThread(); - - ReactContextInitParams initParams = new ReactContextInitParams(jsExecutor, jsBundleLoader); - if (!mIsContextInitAsyncTaskRunning) { - // No background task to create react context is currently running, create and execute one. - ReactContextInitAsyncTask initTask = new ReactContextInitAsyncTask(); - initTask.execute(initParams); - mIsContextInitAsyncTaskRunning = true; - } 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); - moveReactContextToCurrentLifecycleState(reactContext); - - for (ReactRootView rootView : mAttachedRootViews) { - attachMeasuredRootViewToInstance(rootView, catalystInstance); - } - } - - 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 = launchOptions != null - ? Arguments.fromBundle(launchOptions) - : Arguments.createMap(); - 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(ReactNative.class) - .unmountComponentAtNodeAndRemoveContainer(rootView.getId()); - } - - private void tearDownReactContext(ReactContext reactContext) { - UiThreadUtil.assertOnUiThread(); - if (mLifecycleState == LifecycleState.RESUMED) { - reactContext.onPause(); - } - for (ReactRootView rootView : mAttachedRootViews) { - detachViewFromInstance(rootView, reactContext.getCatalystInstance()); - } - reactContext.onDestroy(); - mDevSupportManager.onReactInstanceDestroyed(reactContext); - } + public abstract @Nullable ReactContext getCurrentReactContext(); /** - * @return instance of {@link ReactContext} configured a {@link CatalystInstance} set + * Creates a builder that is capable of creating an instance of {@link ReactInstanceManagerImpl}. */ - private ReactApplicationContext createReactContext( - JavaScriptExecutor jsExecutor, - JSBundleLoader jsBundleLoader) { - FLog.i(ReactConstants.TAG, "Creating react context."); - mSourceUrl = jsBundleLoader.getSourceUrl(); - NativeModuleRegistry.Builder nativeRegistryBuilder = new NativeModuleRegistry.Builder(); - JavaScriptModulesConfig.Builder jsModulesBuilder = new JavaScriptModulesConfig.Builder(); - - ReactApplicationContext reactContext = new ReactApplicationContext(mApplicationContext); - if (mUseDeveloperSupport) { - reactContext.setNativeModuleCallExceptionHandler(mDevSupportManager); - } - - CoreModulesPackage coreModulesPackage = - new CoreModulesPackage(this, mBackBtnHandler); - processPackage(coreModulesPackage, reactContext, nativeRegistryBuilder, jsModulesBuilder); - - // TODO(6818138): Solve use-case of native/js modules overriding - for (ReactPackage reactPackage : mPackages) { - processPackage(reactPackage, reactContext, nativeRegistryBuilder, jsModulesBuilder); - } - - CatalystInstance.Builder catalystInstanceBuilder = new CatalystInstance.Builder() - .setCatalystQueueConfigurationSpec(CatalystQueueConfigurationSpec.createDefault()) - .setJSExecutor(jsExecutor) - .setRegistry(nativeRegistryBuilder.build()) - .setJSModulesConfig(jsModulesBuilder.build()) - .setJSBundleLoader(jsBundleLoader) - .setNativeModuleCallExceptionHandler(mDevSupportManager); - - CatalystInstance catalystInstance = catalystInstanceBuilder.build(); - if (mBridgeIdleDebugListener != null) { - catalystInstance.addBridgeIdleDebugListener(mBridgeIdleDebugListener); - } - - reactContext.initializeWithInstance(catalystInstance); - catalystInstance.runJSBundle(); - - return reactContext; - } - - private void processPackage( - ReactPackage reactPackage, - ReactApplicationContext reactContext, - NativeModuleRegistry.Builder nativeRegistryBuilder, - JavaScriptModulesConfig.Builder jsModulesBuilder) { - for (NativeModule nativeModule : reactPackage.createNativeModules(reactContext)) { - nativeRegistryBuilder.add(nativeModule); - } - for (Class jsModuleClass : reactPackage.createJSModules()) { - jsModulesBuilder.add(jsModuleClass); - } - } - - private void moveReactContextToCurrentLifecycleState(ReactApplicationContext reactContext) { - if (mLifecycleState == LifecycleState.RESUMED) { - reactContext.onResume(mCurrentActivity); - } + public static Builder builder() { + return new Builder(); } /** - * Builder class for {@link ReactInstanceManager} + * Builder class for {@link ReactInstanceManagerImpl} */ public static class Builder { - private final List mPackages = new ArrayList<>(); + protected final List mPackages = new ArrayList<>(); - private @Nullable String mJSBundleFile; - private @Nullable String mJSMainModuleName; - private @Nullable NotThreadSafeBridgeIdleDebugListener mBridgeIdleDebugListener; - private @Nullable Application mApplication; - private boolean mUseDeveloperSupport; - private @Nullable LifecycleState mInitialLifecycleState; + protected @Nullable String mJSBundleFile; + protected @Nullable String mJSMainModuleName; + protected @Nullable NotThreadSafeBridgeIdleDebugListener mBridgeIdleDebugListener; + protected @Nullable Application mApplication; + protected boolean mUseDeveloperSupport; + protected @Nullable LifecycleState mInitialLifecycleState; - private Builder() { + protected Builder() { } /** @@ -718,7 +215,7 @@ public class ReactInstanceManager { } /** - * Instantiates a new {@link ReactInstanceManager}. + * Instantiates a new {@link ReactInstanceManagerImpl}. * Before calling {@code build}, the following must be called: *
    *
  • {@link #setApplication} @@ -734,7 +231,7 @@ public class ReactInstanceManager { mJSMainModuleName != null || mJSBundleFile != null, "Either MainModuleName or JS Bundle File needs to be provided"); - return new ReactInstanceManager( + return new ReactInstanceManagerImpl( Assertions.assertNotNull( mApplication, "Application property has not been set with this builder"), diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManagerImpl.java b/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManagerImpl.java new file mode 100644 index 000000000..f8e0cdc04 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManagerImpl.java @@ -0,0 +1,635 @@ +/** + * 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.List; + +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.Arguments; +import com.facebook.react.bridge.CatalystInstance; +import com.facebook.react.bridge.CatalystInstanceImpl; +import com.facebook.react.bridge.JSBundleLoader; +import com.facebook.react.bridge.JSCJavaScriptExecutor; +import com.facebook.react.bridge.JavaJSExecutor; +import com.facebook.react.bridge.JavaScriptExecutor; +import com.facebook.react.bridge.JavaScriptModule; +import com.facebook.react.bridge.JavaScriptModulesConfig; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.NativeModuleRegistry; +import com.facebook.react.bridge.NotThreadSafeBridgeIdleDebugListener; +import com.facebook.react.bridge.ProxyJavaScriptExecutor; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.UiThreadUtil; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.bridge.WritableNativeMap; +import com.facebook.react.bridge.queue.CatalystQueueConfigurationSpec; +import com.facebook.react.common.ReactConstants; +import com.facebook.react.common.annotations.VisibleForTesting; +import com.facebook.react.devsupport.DevServerHelper; +import com.facebook.react.devsupport.DevSupportManager; +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.ReactNative; +import com.facebook.react.uimanager.UIManagerModule; +import com.facebook.react.uimanager.ViewManager; +import com.facebook.soloader.SoLoader; + +/** + * 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 ReactInstanceManagerImpl} 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 #onPause}, + * {@link #onDestroy} and {@link #onResume}). + * + * To instantiate an instance of this class use {@link #builder}. + */ +/* package */ class ReactInstanceManagerImpl extends ReactInstanceManager { + + /* should only be accessed from main thread (UI thread) */ + private final List mAttachedRootViews = new ArrayList<>(); + private LifecycleState mLifecycleState; + private boolean mIsContextInitAsyncTaskRunning; + private @Nullable ReactContextInitParams mPendingReactContextInitParams; + + /* 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 volatile boolean mHasStartedCreatingInitialContext = false; + + private final ReactInstanceDevCommandsHandler mDevInterface = + new ReactInstanceDevCommandsHandler() { + + @Override + public void onReloadWithJSDebugger(JavaJSExecutor jsExecutor) { + ReactInstanceManagerImpl.this.onReloadWithJSDebugger(jsExecutor); + } + + @Override + public void onJSBundleLoadedFromServer() { + ReactInstanceManagerImpl.this.onJSBundleLoadedFromServer(); + } + + @Override + public void toggleElementInspector() { + ReactInstanceManagerImpl.this.toggleElementInspector(); + } + }; + + private final DefaultHardwareBackBtnHandler mBackBtnHandler = + new DefaultHardwareBackBtnHandler() { + @Override + public void invokeDefaultOnBackPressed() { + ReactInstanceManagerImpl.this.invokeDefaultOnBackPressed(); + } + }; + + private class ReactContextInitParams { + private final JavaScriptExecutor mJsExecutor; + private final JSBundleLoader mJsBundleLoader; + + public ReactContextInitParams( + JavaScriptExecutor jsExecutor, + JSBundleLoader jsBundleLoader) { + mJsExecutor = Assertions.assertNotNull(jsExecutor); + mJsBundleLoader = Assertions.assertNotNull(jsBundleLoader); + } + + public JavaScriptExecutor getJsExecutor() { + return mJsExecutor; + } + + 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 ReactApplicationContext doInBackground(ReactContextInitParams... params) { + Assertions.assertCondition(params != null && params.length > 0 && params[0] != null); + return createReactContext(params[0].getJsExecutor(), params[0].getJsBundleLoader()); + } + + @Override + protected void onPostExecute(ReactApplicationContext reactContext) { + try { + setupReactContext(reactContext); + } finally { + mIsContextInitAsyncTaskRunning = false; + } + + // Handle enqueued request to re-initialize react context. + if (mPendingReactContextInitParams != null) { + recreateReactContextInBackground( + mPendingReactContextInitParams.getJsExecutor(), + mPendingReactContextInitParams.getJsBundleLoader()); + mPendingReactContextInitParams = null; + } + } + } + + /* package */ ReactInstanceManagerImpl( + Context applicationContext, + @Nullable String jsBundleFile, + @Nullable String jsMainModuleName, + List packages, + boolean useDeveloperSupport, + @Nullable NotThreadSafeBridgeIdleDebugListener bridgeIdleDebugListener, + LifecycleState initialLifecycleState) { + initializeSoLoaderIfNecessary(applicationContext); + + mApplicationContext = applicationContext; + mJSBundleFile = jsBundleFile; + mJSMainModuleName = jsMainModuleName; + mPackages = packages; + mUseDeveloperSupport = useDeveloperSupport; + // We need to instantiate DevSupportManager regardless to the useDeveloperSupport option, + // although will prevent dev support manager from displaying any options or dialogs by + // checking useDeveloperSupport option before calling setDevSupportEnabled on this manager + // TODO(6803830): Don't instantiate devsupport manager when useDeveloperSupport is false + mDevSupportManager = new DevSupportManager( + applicationContext, + mDevInterface, + mJSMainModuleName, + useDeveloperSupport); + mBridgeIdleDebugListener = bridgeIdleDebugListener; + mLifecycleState = initialLifecycleState; + } + + @Override + public DevSupportManager getDevSupportManager() { + return mDevSupportManager; + } + + 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 ReactInstanceManagerImpl + 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(), + 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 onPause() { + UiThreadUtil.assertOnUiThread(); + + mLifecycleState = LifecycleState.BEFORE_RESUME; + + mDefaultBackButtonImpl = null; + if (mUseDeveloperSupport) { + mDevSupportManager.setDevSupportEnabled(false); + } + + mCurrentActivity = null; + if (mCurrentReactContext != null) { + mCurrentReactContext.onPause(); + } + } + + /** + * 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 + * ReactInstanceManagerImpl}, so that once this instance receive {@link #onDestroy} event it will + * clear the reference to that defaultBackButtonImpl. + * + * @param defaultBackButtonImpl a {@link DefaultHardwareBackBtnHandler} from an Activity that owns + * this instance of {@link ReactInstanceManagerImpl}. + */ + @Override + public void onResume(Activity activity, DefaultHardwareBackBtnHandler defaultBackButtonImpl) { + UiThreadUtil.assertOnUiThread(); + + mLifecycleState = LifecycleState.RESUMED; + + mDefaultBackButtonImpl = defaultBackButtonImpl; + if (mUseDeveloperSupport) { + mDevSupportManager.setDevSupportEnabled(true); + } + + mCurrentActivity = activity; + if (mCurrentReactContext != null) { + mCurrentReactContext.onResume(activity); + } + } + + @Override + public void onDestroy() { + UiThreadUtil.assertOnUiThread(); + + if (mUseDeveloperSupport) { + mDevSupportManager.setDevSupportEnabled(false); + } + + if (mCurrentReactContext != null) { + mCurrentReactContext.onDestroy(); + mCurrentReactContext = null; + mHasStartedCreatingInitialContext = false; + } + } + + @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 (!mIsContextInitAsyncTaskRunning && 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) { + List allViewManagers = new ArrayList<>(); + for (ReactPackage reactPackage : mPackages) { + allViewManagers.addAll(reactPackage.createViewManagers(catalystApplicationContext)); + } + return allViewManagers; + } + + @VisibleForTesting + @Override + public @Nullable ReactContext getCurrentReactContext() { + return mCurrentReactContext; + } + + private void onReloadWithJSDebugger(JavaJSExecutor jsExecutor) { + recreateReactContextInBackground( + new ProxyJavaScriptExecutor(jsExecutor), + JSBundleLoader.createRemoteDebuggerBundleLoader( + mDevSupportManager.getJSBundleURLForRemoteDebugging())); + } + + private void onJSBundleLoadedFromServer() { + recreateReactContextInBackground( + new JSCJavaScriptExecutor(), + JSBundleLoader.createCachedBundleFromNetworkLoader( + mDevSupportManager.getSourceUrl(), + mDevSupportManager.getDownloadedJSBundleFile())); + } + + private void recreateReactContextInBackground( + JavaScriptExecutor jsExecutor, + JSBundleLoader jsBundleLoader) { + UiThreadUtil.assertOnUiThread(); + + ReactContextInitParams initParams = new ReactContextInitParams(jsExecutor, jsBundleLoader); + if (!mIsContextInitAsyncTaskRunning) { + // No background task to create react context is currently running, create and execute one. + ReactContextInitAsyncTask initTask = new ReactContextInitAsyncTask(); + initTask.execute(initParams); + mIsContextInitAsyncTaskRunning = true; + } 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); + moveReactContextToCurrentLifecycleState(reactContext); + + for (ReactRootView rootView : mAttachedRootViews) { + attachMeasuredRootViewToInstance(rootView, catalystInstance); + } + } + + 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 = launchOptions != null + ? Arguments.fromBundle(launchOptions) + : Arguments.createMap(); + 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(ReactNative.class) + .unmountComponentAtNodeAndRemoveContainer(rootView.getId()); + } + + private void tearDownReactContext(ReactContext reactContext) { + UiThreadUtil.assertOnUiThread(); + if (mLifecycleState == LifecycleState.RESUMED) { + reactContext.onPause(); + } + for (ReactRootView rootView : mAttachedRootViews) { + detachViewFromInstance(rootView, reactContext.getCatalystInstance()); + } + reactContext.onDestroy(); + mDevSupportManager.onReactInstanceDestroyed(reactContext); + } + + /** + * @return instance of {@link ReactContext} configured a {@link CatalystInstance} set + */ + private ReactApplicationContext createReactContext( + JavaScriptExecutor jsExecutor, + JSBundleLoader jsBundleLoader) { + FLog.i(ReactConstants.TAG, "Creating react context."); + mSourceUrl = jsBundleLoader.getSourceUrl(); + NativeModuleRegistry.Builder nativeRegistryBuilder = new NativeModuleRegistry.Builder(); + JavaScriptModulesConfig.Builder jsModulesBuilder = new JavaScriptModulesConfig.Builder(); + + ReactApplicationContext reactContext = new ReactApplicationContext(mApplicationContext); + if (mUseDeveloperSupport) { + reactContext.setNativeModuleCallExceptionHandler(mDevSupportManager); + } + + CoreModulesPackage coreModulesPackage = + new CoreModulesPackage(this, mBackBtnHandler); + processPackage(coreModulesPackage, reactContext, nativeRegistryBuilder, jsModulesBuilder); + + // TODO(6818138): Solve use-case of native/js modules overriding + for (ReactPackage reactPackage : mPackages) { + processPackage(reactPackage, reactContext, nativeRegistryBuilder, jsModulesBuilder); + } + + CatalystInstanceImpl.Builder catalystInstanceBuilder = new CatalystInstanceImpl.Builder() + .setCatalystQueueConfigurationSpec(CatalystQueueConfigurationSpec.createDefault()) + .setJSExecutor(jsExecutor) + .setRegistry(nativeRegistryBuilder.build()) + .setJSModulesConfig(jsModulesBuilder.build()) + .setJSBundleLoader(jsBundleLoader) + .setNativeModuleCallExceptionHandler(mDevSupportManager); + + CatalystInstance catalystInstance = catalystInstanceBuilder.build(); + if (mBridgeIdleDebugListener != null) { + catalystInstance.addBridgeIdleDebugListener(mBridgeIdleDebugListener); + } + + reactContext.initializeWithInstance(catalystInstance); + catalystInstance.runJSBundle(); + + return reactContext; + } + + private void processPackage( + ReactPackage reactPackage, + ReactApplicationContext reactContext, + NativeModuleRegistry.Builder nativeRegistryBuilder, + JavaScriptModulesConfig.Builder jsModulesBuilder) { + for (NativeModule nativeModule : reactPackage.createNativeModules(reactContext)) { + nativeRegistryBuilder.add(nativeModule); + } + for (Class jsModuleClass : reactPackage.createJSModules()) { + jsModulesBuilder.add(jsModuleClass); + } + } + + private void moveReactContextToCurrentLifecycleState(ReactApplicationContext reactContext) { + if (mLifecycleState == LifecycleState.RESUMED) { + reactContext.onResume(mCurrentActivity); + } + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/CatalystInstance.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/CatalystInstance.java index ee9138022..7e5afc3a4 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/CatalystInstance.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/CatalystInstance.java @@ -11,27 +11,11 @@ package com.facebook.react.bridge; import javax.annotation.Nullable; -import java.io.IOException; -import java.io.StringWriter; import java.util.Collection; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; -import com.facebook.common.logging.FLog; import com.facebook.react.bridge.queue.CatalystQueueConfiguration; -import com.facebook.react.bridge.queue.CatalystQueueConfigurationSpec; -import com.facebook.react.bridge.queue.QueueThreadExceptionHandler; 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.systrace.Systrace; -import com.facebook.systrace.TraceListener; - -import com.fasterxml.jackson.core.JsonFactory; -import com.fasterxml.jackson.core.JsonGenerator; /** * A higher level API on top of the asynchronous JSC bridge. This provides an @@ -39,254 +23,31 @@ import com.fasterxml.jackson.core.JsonGenerator; * Java APIs be invokable from JavaScript as well. */ @DoNotStrip -public class CatalystInstance { - - 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 CatalystQueueConfiguration mCatalystQueueConfiguration; - 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; - - // Access from native modules thread - private final NativeModuleRegistry mJavaRegistry; - private final NativeModuleCallExceptionHandler mNativeModuleCallExceptionHandler; - private boolean mInitialized = false; - - // Access from JS thread - private @Nullable ReactBridge mBridge; - private boolean mJSBundleHasLoaded; - - private CatalystInstance( - final CatalystQueueConfigurationSpec catalystQueueConfigurationSpec, - final JavaScriptExecutor jsExecutor, - final NativeModuleRegistry registry, - final JavaScriptModulesConfig jsModulesConfig, - final JSBundleLoader jsBundleLoader, - NativeModuleCallExceptionHandler nativeModuleCallExceptionHandler) { - mCatalystQueueConfiguration = CatalystQueueConfiguration.create( - catalystQueueConfigurationSpec, - new NativeExceptionHandler()); - mBridgeIdleListeners = new CopyOnWriteArrayList<>(); - mJavaRegistry = registry; - mJSModuleRegistry = new JavaScriptModuleRegistry(CatalystInstance.this, jsModulesConfig); - mJSBundleLoader = jsBundleLoader; - mNativeModuleCallExceptionHandler = nativeModuleCallExceptionHandler; - mTraceListener = new JSProfilerTraceListener(); - - final CountDownLatch initLatch = new CountDownLatch(1); - mCatalystQueueConfiguration.getJSQueueThread().runOnQueue( - new Runnable() { - @Override - public void run() { - initializeBridge(jsExecutor, jsModulesConfig); - initLatch.countDown(); - } - }); - - try { - Assertions.assertCondition( - initLatch.await(BRIDGE_SETUP_TIMEOUT_MS, TimeUnit.MILLISECONDS), - "Timed out waiting for bridge to initialize!"); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - } - - private void initializeBridge( - JavaScriptExecutor jsExecutor, - JavaScriptModulesConfig jsModulesConfig) { - mCatalystQueueConfiguration.getJSQueueThread().assertIsOnThread(); - Assertions.assertCondition(mBridge == null, "initializeBridge should be called once"); - - mBridge = new ReactBridge( - jsExecutor, - new NativeModulesReactCallback(), - mCatalystQueueConfiguration.getNativeModulesQueueThread()); - mBridge.setGlobalVariable( - "__fbBatchedBridgeConfig", - buildModulesConfigJSONProperty(mJavaRegistry, jsModulesConfig)); - - Systrace.registerListener(mTraceListener); - } - - public void runJSBundle() { - Systrace.beginSection( - Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, - "CatalystInstance_runJSBundle"); - - try { - final CountDownLatch initLatch = new CountDownLatch(1); - mCatalystQueueConfiguration.getJSQueueThread().runOnQueue( - new Runnable() { - @Override - public void run() { - Assertions.assertCondition(!mJSBundleHasLoaded, "JS bundle was already loaded!"); - mJSBundleHasLoaded = true; - - incrementPendingJSCalls(); - - try { - mJSBundleLoader.loadScript(mBridge); - } catch (JSExecutionException e) { - mNativeModuleCallExceptionHandler.handleException(e); - } - - initLatch.countDown(); - } - }); - Assertions.assertCondition( - initLatch.await(LOAD_JS_BUNDLE_TIMEOUT_MS, TimeUnit.MILLISECONDS), - "Timed out loading JS!"); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } finally { - Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); - } - } - - /* package */ void callFunction( - final int moduleId, - final int methodId, - final NativeArray arguments, - final String tracingName) { - if (mDestroyed) { - FLog.w(ReactConstants.TAG, "Calling JS function after bridge has been destroyed."); - return; - } - - incrementPendingJSCalls(); - - mCatalystQueueConfiguration.getJSQueueThread().runOnQueue( - new Runnable() { - @Override - public void run() { - mCatalystQueueConfiguration.getJSQueueThread().assertIsOnThread(); - - if (mDestroyed) { - return; - } - - Systrace.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, tracingName); - try { - Assertions.assertNotNull(mBridge).callFunction(moduleId, methodId, arguments); - } finally { - Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); - } - } - }); - } - +public interface CatalystInstance { + void runJSBundle(); // This is called from java code, so it won't be stripped anyway, but proguard will rename it, // which this prevents. @DoNotStrip - /* package */ void invokeCallback(final int callbackID, final NativeArray arguments) { - if (mDestroyed) { - FLog.w(ReactConstants.TAG, "Invoking JS callback after bridge has been destroyed."); - return; - } - - incrementPendingJSCalls(); - - mCatalystQueueConfiguration.getJSQueueThread().runOnQueue( - new Runnable() { - @Override - public void run() { - mCatalystQueueConfiguration.getJSQueueThread().assertIsOnThread(); - - if (mDestroyed) { - return; - } - - Systrace.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, ""); - try { - Assertions.assertNotNull(mBridge).invokeCallback(callbackID, arguments); - } finally { - Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); - } - } - }); - } - + void invokeCallback(final int callbackID, final NativeArray arguments); /** * Destroys this catalyst instance, waiting for any other threads in CatalystQueueConfiguration * (besides the UI thread) to finish running. Must be called from the UI thread so that we can * fully shut down other threads. */ - /* package */ void destroy() { - UiThreadUtil.assertOnUiThread(); - - if (mDestroyed) { - return; - } - - // TODO: tell all APIs to shut down - mDestroyed = true; - mJavaRegistry.notifyCatalystInstanceDestroy(); - mCatalystQueueConfiguration.destroy(); - boolean wasIdle = (mPendingJSCalls.getAndSet(0) == 0); - if (!wasIdle && !mBridgeIdleListeners.isEmpty()) { - for (NotThreadSafeBridgeIdleDebugListener listener : mBridgeIdleListeners) { - listener.onTransitionToBridgeIdle(); - } - } - - if (mBridge != null) { - Systrace.unregisterListener(mTraceListener); - } - - // We can access the Bridge from any thread now because we know either we are on the JS thread - // or the JS thread has finished via CatalystQueueConfiguration#destroy() - Assertions.assertNotNull(mBridge).dispose(); - } - - public boolean isDestroyed() { - return mDestroyed; - } + void destroy(); + boolean isDestroyed(); /** * Initialize all the native modules */ @VisibleForTesting - public void initialize() { - UiThreadUtil.assertOnUiThread(); - Assertions.assertCondition( - !mInitialized, - "This catalyst instance has already been initialized"); - mInitialized = true; - mJavaRegistry.notifyCatalystInstanceInitialized(); - } + void initialize(); - public CatalystQueueConfiguration getCatalystQueueConfiguration() { - return mCatalystQueueConfiguration; - } + CatalystQueueConfiguration getCatalystQueueConfiguration(); - @VisibleForTesting - public @Nullable - ReactBridge getBridge() { - return mBridge; - } - - public T getJSModule(Class jsInterface) { - return Assertions.assertNotNull(mJSModuleRegistry).getJavaScriptModule(jsInterface); - } - - public T getNativeModule(Class nativeModuleInterface) { - return mJavaRegistry.getModule(nativeModuleInterface); - } - - public Collection getNativeModules() { - return mJavaRegistry.getAllModules(); - } + T getJSModule(Class jsInterface); + T getNativeModule(Class nativeModuleInterface); + Collection getNativeModules(); /** * Adds a idle listener for this Catalyst instance. The listener will receive notifications @@ -294,204 +55,15 @@ public class CatalystInstance { * defined as there being some non-zero number of calls to JS that haven't resolved via a * onBatchCompleted call. The listener should be purely passive and not affect application logic. */ - public void addBridgeIdleDebugListener(NotThreadSafeBridgeIdleDebugListener listener) { - mBridgeIdleListeners.add(listener); - } + void addBridgeIdleDebugListener(NotThreadSafeBridgeIdleDebugListener listener); /** * Removes a NotThreadSafeBridgeIdleDebugListener previously added with * {@link #addBridgeIdleDebugListener} */ - public void removeBridgeIdleDebugListener(NotThreadSafeBridgeIdleDebugListener listener) { - mBridgeIdleListeners.remove(listener); - } + void removeBridgeIdleDebugListener(NotThreadSafeBridgeIdleDebugListener listener); - public boolean supportsProfiling() { - if (mBridge == null) { - return false; - } - return mBridge.supportsProfiling(); - } - - public void startProfiler(String title) { - if (mBridge == null) { - return; - } - mBridge.startProfiler(title); - } - - public void stopProfiler(String title, String filename) { - if (mBridge == null) { - return; - } - mBridge.stopProfiler(title, filename); - } - - private String buildModulesConfigJSONProperty( - NativeModuleRegistry nativeModuleRegistry, - JavaScriptModulesConfig jsModulesConfig) { - // TODO(5300733): Serialize config using single json generator - JsonFactory jsonFactory = new JsonFactory(); - StringWriter writer = new StringWriter(); - try { - JsonGenerator jg = jsonFactory.createGenerator(writer); - jg.writeStartObject(); - jg.writeFieldName("remoteModuleConfig"); - jg.writeRawValue(nativeModuleRegistry.moduleDescriptions()); - jg.writeFieldName("localModulesConfig"); - jg.writeRawValue(jsModulesConfig.moduleDescriptions()); - jg.writeEndObject(); - jg.close(); - } catch (IOException ioe) { - throw new RuntimeException("Unable to serialize JavaScript module declaration", ioe); - } - return writer.getBuffer().toString(); - } - - 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(); - 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 class NativeModulesReactCallback implements ReactCallback { - - @Override - public void call(int moduleId, int methodId, ReadableNativeArray parameters) { - mCatalystQueueConfiguration.getNativeModulesQueueThread().assertIsOnThread(); - - // Suppress any callbacks if destroyed - will only lead to sadness. - if (mDestroyed) { - return; - } - - mJavaRegistry.call(CatalystInstance.this, moduleId, methodId, parameters); - } - - @Override - public void onBatchComplete() { - mCatalystQueueConfiguration.getNativeModulesQueueThread().assertIsOnThread(); - - // The bridge may have been destroyed due to an exception during the batch. In that case - // native modules could be in a bad state so we don't want to call anything on them. We - // still want to trigger the debug listener since it allows instrumentation tests to end and - // check their assertions without waiting for a timeout. - if (!mDestroyed) { - Systrace.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "onBatchComplete"); - try { - mJavaRegistry.onBatchComplete(); - } finally { - Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); - } - } - - decrementPendingJSCalls(); - } - } - - 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. - mNativeModuleCallExceptionHandler.handleException(e); - mCatalystQueueConfiguration.getUIQueueThread().runOnQueue( - new Runnable() { - @Override - public void run() { - destroy(); - } - }); - } - } - - private class JSProfilerTraceListener implements TraceListener { - @Override - public void onTraceStarted() { - getJSModule(BridgeProfiling.class).setEnabled(true); - } - - @Override - public void onTraceStopped() { - getJSModule(BridgeProfiling.class).setEnabled(false); - } - } - - public static class Builder { - - private @Nullable CatalystQueueConfigurationSpec mCatalystQueueConfigurationSpec; - private @Nullable JSBundleLoader mJSBundleLoader; - private @Nullable NativeModuleRegistry mRegistry; - private @Nullable JavaScriptModulesConfig mJSModulesConfig; - private @Nullable JavaScriptExecutor mJSExecutor; - private @Nullable NativeModuleCallExceptionHandler mNativeModuleCallExceptionHandler; - - public Builder setCatalystQueueConfigurationSpec( - CatalystQueueConfigurationSpec catalystQueueConfigurationSpec) { - mCatalystQueueConfigurationSpec = catalystQueueConfigurationSpec; - return this; - } - - public Builder setRegistry(NativeModuleRegistry registry) { - mRegistry = registry; - return this; - } - - public Builder setJSModulesConfig(JavaScriptModulesConfig jsModulesConfig) { - mJSModulesConfig = jsModulesConfig; - 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 CatalystInstance build() { - return new CatalystInstance( - Assertions.assertNotNull(mCatalystQueueConfigurationSpec), - Assertions.assertNotNull(mJSExecutor), - Assertions.assertNotNull(mRegistry), - Assertions.assertNotNull(mJSModulesConfig), - Assertions.assertNotNull(mJSBundleLoader), - Assertions.assertNotNull(mNativeModuleCallExceptionHandler)); - } - } + boolean supportsProfiling(); + void startProfiler(String title); + void stopProfiler(String title, String filename); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/CatalystInstanceImpl.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/CatalystInstanceImpl.java new file mode 100644 index 000000000..4b987e78d --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/CatalystInstanceImpl.java @@ -0,0 +1,510 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.bridge; + +import javax.annotation.Nullable; + +import java.io.IOException; +import java.io.StringWriter; +import java.util.Collection; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import com.facebook.common.logging.FLog; +import com.facebook.react.bridge.queue.CatalystQueueConfiguration; +import com.facebook.react.bridge.queue.CatalystQueueConfigurationImpl; +import com.facebook.react.bridge.queue.CatalystQueueConfigurationSpec; +import com.facebook.react.bridge.queue.QueueThreadExceptionHandler; +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.systrace.Systrace; +import com.facebook.systrace.TraceListener; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonGenerator; + +/** + * This provides an implementation of the public CatalystInstance instance. It is public because + * it is built by ReactInstanceManager which is in a different package. + */ +@DoNotStrip +public class CatalystInstanceImpl implements CatalystInstance { + + 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 CatalystQueueConfigurationImpl mCatalystQueueConfiguration; + 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; + + // Access from native modules thread + private final NativeModuleRegistry mJavaRegistry; + private final NativeModuleCallExceptionHandler mNativeModuleCallExceptionHandler; + private boolean mInitialized = false; + + // Access from JS thread + private @Nullable ReactBridge mBridge; + private boolean mJSBundleHasLoaded; + + private CatalystInstanceImpl( + final CatalystQueueConfigurationSpec catalystQueueConfigurationSpec, + final JavaScriptExecutor jsExecutor, + final NativeModuleRegistry registry, + final JavaScriptModulesConfig jsModulesConfig, + final JSBundleLoader jsBundleLoader, + NativeModuleCallExceptionHandler nativeModuleCallExceptionHandler) { + mCatalystQueueConfiguration = CatalystQueueConfigurationImpl.create( + catalystQueueConfigurationSpec, + new NativeExceptionHandler()); + mBridgeIdleListeners = new CopyOnWriteArrayList<>(); + mJavaRegistry = registry; + mJSModuleRegistry = new JavaScriptModuleRegistry(CatalystInstanceImpl.this, jsModulesConfig); + mJSBundleLoader = jsBundleLoader; + mNativeModuleCallExceptionHandler = nativeModuleCallExceptionHandler; + mTraceListener = new JSProfilerTraceListener(); + + final CountDownLatch initLatch = new CountDownLatch(1); + mCatalystQueueConfiguration.getJSQueueThread().runOnQueue( + new Runnable() { + @Override + public void run() { + initializeBridge(jsExecutor, jsModulesConfig); + initLatch.countDown(); + } + }); + + try { + Assertions.assertCondition( + initLatch.await(BRIDGE_SETUP_TIMEOUT_MS, TimeUnit.MILLISECONDS), + "Timed out waiting for bridge to initialize!"); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + private void initializeBridge( + JavaScriptExecutor jsExecutor, + JavaScriptModulesConfig jsModulesConfig) { + mCatalystQueueConfiguration.getJSQueueThread().assertIsOnThread(); + Assertions.assertCondition(mBridge == null, "initializeBridge should be called once"); + + mBridge = new ReactBridge( + jsExecutor, + new NativeModulesReactCallback(), + mCatalystQueueConfiguration.getNativeModulesQueueThread()); + mBridge.setGlobalVariable( + "__fbBatchedBridgeConfig", + buildModulesConfigJSONProperty(mJavaRegistry, jsModulesConfig)); + Systrace.registerListener(mTraceListener); + } + + @Override + public void runJSBundle() { + Systrace.beginSection( + Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, + "CatalystInstance_runJSBundle"); + + try { + final CountDownLatch initLatch = new CountDownLatch(1); + mCatalystQueueConfiguration.getJSQueueThread().runOnQueue( + new Runnable() { + @Override + public void run() { + Assertions.assertCondition(!mJSBundleHasLoaded, "JS bundle was already loaded!"); + mJSBundleHasLoaded = true; + + incrementPendingJSCalls(); + + try { + mJSBundleLoader.loadScript(mBridge); + } catch (JSExecutionException e) { + mNativeModuleCallExceptionHandler.handleException(e); + } + + initLatch.countDown(); + } + }); + Assertions.assertCondition( + initLatch.await(LOAD_JS_BUNDLE_TIMEOUT_MS, TimeUnit.MILLISECONDS), + "Timed out loading JS!"); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } finally { + Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); + } + } + + /* package */ void callFunction( + final int moduleId, + final int methodId, + final NativeArray arguments, + final String tracingName) { + if (mDestroyed) { + FLog.w(ReactConstants.TAG, "Calling JS function after bridge has been destroyed."); + return; + } + + incrementPendingJSCalls(); + + mCatalystQueueConfiguration.getJSQueueThread().runOnQueue( + new Runnable() { + @Override + public void run() { + mCatalystQueueConfiguration.getJSQueueThread().assertIsOnThread(); + + if (mDestroyed) { + return; + } + + Systrace.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, tracingName); + try { + Assertions.assertNotNull(mBridge).callFunction(moduleId, methodId, arguments); + } finally { + Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); + } + } + }); + } + + // This is called from java code, so it won't be stripped anyway, but proguard will rename it, + // which this prevents. + @DoNotStrip + @Override + public void invokeCallback(final int callbackID, final NativeArray arguments) { + if (mDestroyed) { + FLog.w(ReactConstants.TAG, "Invoking JS callback after bridge has been destroyed."); + return; + } + + incrementPendingJSCalls(); + + mCatalystQueueConfiguration.getJSQueueThread().runOnQueue( + new Runnable() { + @Override + public void run() { + mCatalystQueueConfiguration.getJSQueueThread().assertIsOnThread(); + + if (mDestroyed) { + return; + } + + Systrace.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, ""); + try { + Assertions.assertNotNull(mBridge).invokeCallback(callbackID, arguments); + } finally { + Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); + } + } + }); + } + + /** + * Destroys this catalyst instance, waiting for any other threads in CatalystQueueConfiguration + * (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; + mJavaRegistry.notifyCatalystInstanceDestroy(); + mCatalystQueueConfiguration.destroy(); + boolean wasIdle = (mPendingJSCalls.getAndSet(0) == 0); + if (!wasIdle && !mBridgeIdleListeners.isEmpty()) { + for (NotThreadSafeBridgeIdleDebugListener listener : mBridgeIdleListeners) { + listener.onTransitionToBridgeIdle(); + } + } + + if (mBridge != null) { + Systrace.unregisterListener(mTraceListener); + } + + // We can access the Bridge from any thread now because we know either we are on the JS thread + // or the JS thread has finished via CatalystQueueConfiguration#destroy() + Assertions.assertNotNull(mBridge).dispose(); + } + + @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 CatalystQueueConfiguration getCatalystQueueConfiguration() { + return mCatalystQueueConfiguration; + } + + @VisibleForTesting + public @Nullable + ReactBridge getBridge() { + return mBridge; + } + + @Override + public T getJSModule(Class jsInterface) { + return Assertions.assertNotNull(mJSModuleRegistry).getJavaScriptModule(jsInterface); + } + + @Override + public T getNativeModule(Class nativeModuleInterface) { + return mJavaRegistry.getModule(nativeModuleInterface); + } + + @Override + public Collection getNativeModules() { + return mJavaRegistry.getAllModules(); + } + + /** + * Adds a idle listener for this Catalyst instance. The listener will receive notifications + * whenever the bridge transitions from idle to busy and vice-versa, where the busy state is + * defined as there being some non-zero number of calls to JS that haven't resolved via a + * onBatchCompleted call. The listener should be purely passive and not affect application logic. + */ + @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 boolean supportsProfiling() { + if (mBridge == null) { + return false; + } + return mBridge.supportsProfiling(); + } + + @Override + public void startProfiler(String title) { + if (mBridge == null) { + return; + } + mBridge.startProfiler(title); + } + + @Override + public void stopProfiler(String title, String filename) { + if (mBridge == null) { + return; + } + mBridge.stopProfiler(title, filename); + } + + private String buildModulesConfigJSONProperty( + NativeModuleRegistry nativeModuleRegistry, + JavaScriptModulesConfig jsModulesConfig) { + // TODO(5300733): Serialize config using single json generator + JsonFactory jsonFactory = new JsonFactory(); + StringWriter writer = new StringWriter(); + try { + JsonGenerator jg = jsonFactory.createGenerator(writer); + jg.writeStartObject(); + jg.writeFieldName("remoteModuleConfig"); + jg.writeRawValue(nativeModuleRegistry.moduleDescriptions()); + jg.writeFieldName("localModulesConfig"); + jg.writeRawValue(jsModulesConfig.moduleDescriptions()); + jg.writeEndObject(); + jg.close(); + } catch (IOException ioe) { + throw new RuntimeException("Unable to serialize JavaScript module declaration", ioe); + } + return writer.getBuffer().toString(); + } + + 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(); + 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 class NativeModulesReactCallback implements ReactCallback { + + @Override + public void call(int moduleId, int methodId, ReadableNativeArray parameters) { + mCatalystQueueConfiguration.getNativeModulesQueueThread().assertIsOnThread(); + + // Suppress any callbacks if destroyed - will only lead to sadness. + if (mDestroyed) { + return; + } + + mJavaRegistry.call(CatalystInstanceImpl.this, moduleId, methodId, parameters); + } + + @Override + public void onBatchComplete() { + mCatalystQueueConfiguration.getNativeModulesQueueThread().assertIsOnThread(); + + // The bridge may have been destroyed due to an exception during the batch. In that case + // native modules could be in a bad state so we don't want to call anything on them. We + // still want to trigger the debug listener since it allows instrumentation tests to end and + // check their assertions without waiting for a timeout. + if (!mDestroyed) { + Systrace.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "onBatchComplete"); + try { + mJavaRegistry.onBatchComplete(); + } finally { + Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); + } + } + + decrementPendingJSCalls(); + } + } + + 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. + mNativeModuleCallExceptionHandler.handleException(e); + mCatalystQueueConfiguration.getUIQueueThread().runOnQueue( + new Runnable() { + @Override + public void run() { + destroy(); + } + }); + } + } + + private class JSProfilerTraceListener implements TraceListener { + @Override + public void onTraceStarted() { + getJSModule(BridgeProfiling.class).setEnabled(true); + } + + @Override + public void onTraceStopped() { + getJSModule(BridgeProfiling.class).setEnabled(false); + } + } + + public static class Builder { + + private @Nullable CatalystQueueConfigurationSpec mCatalystQueueConfigurationSpec; + private @Nullable JSBundleLoader mJSBundleLoader; + private @Nullable NativeModuleRegistry mRegistry; + private @Nullable JavaScriptModulesConfig mJSModulesConfig; + private @Nullable JavaScriptExecutor mJSExecutor; + private @Nullable NativeModuleCallExceptionHandler mNativeModuleCallExceptionHandler; + + public Builder setCatalystQueueConfigurationSpec( + CatalystQueueConfigurationSpec catalystQueueConfigurationSpec) { + mCatalystQueueConfigurationSpec = catalystQueueConfigurationSpec; + return this; + } + + public Builder setRegistry(NativeModuleRegistry registry) { + mRegistry = registry; + return this; + } + + public Builder setJSModulesConfig(JavaScriptModulesConfig jsModulesConfig) { + mJSModulesConfig = jsModulesConfig; + 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(mCatalystQueueConfigurationSpec), + Assertions.assertNotNull(mJSExecutor), + Assertions.assertNotNull(mRegistry), + Assertions.assertNotNull(mJSModulesConfig), + Assertions.assertNotNull(mJSBundleLoader), + Assertions.assertNotNull(mNativeModuleCallExceptionHandler)); + } + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/JavaScriptModuleRegistry.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/JavaScriptModuleRegistry.java index fab0f231e..093770fe0 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/JavaScriptModuleRegistry.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/JavaScriptModuleRegistry.java @@ -30,7 +30,7 @@ import com.facebook.infer.annotation.Assertions; private final HashMap, JavaScriptModule> mModuleInstances; public JavaScriptModuleRegistry( - CatalystInstance instance, + CatalystInstanceImpl instance, JavaScriptModulesConfig config) { mModuleInstances = new HashMap<>(); for (JavaScriptModuleRegistration registration : config.getModuleDefinitions()) { @@ -52,11 +52,11 @@ import com.facebook.infer.annotation.Assertions; private static class JavaScriptModuleInvocationHandler implements InvocationHandler { - private final CatalystInstance mCatalystInstance; + private final CatalystInstanceImpl mCatalystInstance; private final JavaScriptModuleRegistration mModuleRegistration; public JavaScriptModuleInvocationHandler( - CatalystInstance catalystInstance, + CatalystInstanceImpl catalystInstance, JavaScriptModuleRegistration moduleRegistration) { mCatalystInstance = catalystInstance; mModuleRegistration = moduleRegistration; diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/queue/CatalystQueueConfiguration.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/queue/CatalystQueueConfiguration.java index 10be2a44d..99fec7485 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/queue/CatalystQueueConfiguration.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/queue/CatalystQueueConfiguration.java @@ -9,12 +9,6 @@ package com.facebook.react.bridge.queue; -import java.util.Map; - -import android.os.Looper; - -import com.facebook.react.common.MapBuilder; - /** * Specifies which {@link MessageQueueThread}s must be used to run the various contexts of * execution within catalyst (Main UI thread, native modules, and JS). Some of these queues *may* be @@ -24,67 +18,8 @@ import com.facebook.react.common.MapBuilder; * Native Modules Queue Thread: The thread and Looper that native modules are invoked on. * JS Queue Thread: The thread and Looper that JS is executed on. */ -public class CatalystQueueConfiguration { - - private final MessageQueueThread mUIQueueThread; - private final MessageQueueThread mNativeModulesQueueThread; - private final MessageQueueThread mJSQueueThread; - - private CatalystQueueConfiguration( - MessageQueueThread uiQueueThread, - MessageQueueThread nativeModulesQueueThread, - MessageQueueThread jsQueueThread) { - mUIQueueThread = uiQueueThread; - mNativeModulesQueueThread = nativeModulesQueueThread; - mJSQueueThread = jsQueueThread; - } - - public MessageQueueThread getUIQueueThread() { - return mUIQueueThread; - } - - public MessageQueueThread getNativeModulesQueueThread() { - return mNativeModulesQueueThread; - } - - public MessageQueueThread getJSQueueThread() { - return mJSQueueThread; - } - - /** - * Should be called when the corresponding {@link com.facebook.react.bridge.CatalystInstance} - * is destroyed so that we shut down the proper queue threads. - */ - public void destroy() { - if (mNativeModulesQueueThread.getLooper() != Looper.getMainLooper()) { - mNativeModulesQueueThread.quitSynchronous(); - } - if (mJSQueueThread.getLooper() != Looper.getMainLooper()) { - mJSQueueThread.quitSynchronous(); - } - } - - public static CatalystQueueConfiguration create( - CatalystQueueConfigurationSpec spec, - QueueThreadExceptionHandler exceptionHandler) { - Map specsToThreads = MapBuilder.newHashMap(); - - MessageQueueThreadSpec uiThreadSpec = MessageQueueThreadSpec.mainThreadSpec(); - MessageQueueThread uiThread = MessageQueueThread.create( uiThreadSpec, exceptionHandler); - specsToThreads.put(uiThreadSpec, uiThread); - - MessageQueueThread jsThread = specsToThreads.get(spec.getJSQueueThreadSpec()); - if (jsThread == null) { - jsThread = MessageQueueThread.create(spec.getJSQueueThreadSpec(), exceptionHandler); - } - - MessageQueueThread nativeModulesThread = - specsToThreads.get(spec.getNativeModulesQueueThreadSpec()); - if (nativeModulesThread == null) { - nativeModulesThread = - MessageQueueThread.create(spec.getNativeModulesQueueThreadSpec(), exceptionHandler); - } - - return new CatalystQueueConfiguration(uiThread, nativeModulesThread, jsThread); - } +public interface CatalystQueueConfiguration { + MessageQueueThread getUIQueueThread(); + MessageQueueThread getNativeModulesQueueThread(); + MessageQueueThread getJSQueueThread(); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/queue/CatalystQueueConfigurationImpl.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/queue/CatalystQueueConfigurationImpl.java new file mode 100644 index 000000000..c6b8be58b --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/queue/CatalystQueueConfigurationImpl.java @@ -0,0 +1,85 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.bridge.queue; + +import java.util.Map; + +import android.os.Looper; + +import com.facebook.react.common.MapBuilder; + +public class CatalystQueueConfigurationImpl implements CatalystQueueConfiguration { + + private final MessageQueueThreadImpl mUIQueueThread; + private final MessageQueueThreadImpl mNativeModulesQueueThread; + private final MessageQueueThreadImpl mJSQueueThread; + + private CatalystQueueConfigurationImpl( + MessageQueueThreadImpl uiQueueThread, + MessageQueueThreadImpl nativeModulesQueueThread, + MessageQueueThreadImpl jsQueueThread) { + mUIQueueThread = uiQueueThread; + mNativeModulesQueueThread = nativeModulesQueueThread; + mJSQueueThread = jsQueueThread; + } + + @Override + public MessageQueueThread getUIQueueThread() { + return mUIQueueThread; + } + + @Override + public MessageQueueThread getNativeModulesQueueThread() { + return mNativeModulesQueueThread; + } + + @Override + public MessageQueueThread getJSQueueThread() { + return mJSQueueThread; + } + + /** + * Should be called when the corresponding {@link com.facebook.react.bridge.CatalystInstance} + * is destroyed so that we shut down the proper queue threads. + */ + public void destroy() { + if (mNativeModulesQueueThread.getLooper() != Looper.getMainLooper()) { + mNativeModulesQueueThread.quitSynchronous(); + } + if (mJSQueueThread.getLooper() != Looper.getMainLooper()) { + mJSQueueThread.quitSynchronous(); + } + } + + public static CatalystQueueConfigurationImpl create( + CatalystQueueConfigurationSpec spec, + QueueThreadExceptionHandler exceptionHandler) { + Map specsToThreads = MapBuilder.newHashMap(); + + MessageQueueThreadSpec uiThreadSpec = MessageQueueThreadSpec.mainThreadSpec(); + MessageQueueThreadImpl uiThread = + MessageQueueThreadImpl.create( uiThreadSpec, exceptionHandler); + specsToThreads.put(uiThreadSpec, uiThread); + + MessageQueueThreadImpl jsThread = specsToThreads.get(spec.getJSQueueThreadSpec()); + if (jsThread == null) { + jsThread = MessageQueueThreadImpl.create(spec.getJSQueueThreadSpec(), exceptionHandler); + } + + MessageQueueThreadImpl nativeModulesThread = + specsToThreads.get(spec.getNativeModulesQueueThreadSpec()); + if (nativeModulesThread == null) { + nativeModulesThread = + MessageQueueThreadImpl.create(spec.getNativeModulesQueueThreadSpec(), exceptionHandler); + } + + return new CatalystQueueConfigurationImpl(uiThread, nativeModulesThread, jsThread); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/queue/MessageQueueThread.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/queue/MessageQueueThread.java index 0090b82ad..b04285a6f 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/queue/MessageQueueThread.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/queue/MessageQueueThread.java @@ -9,136 +9,28 @@ package com.facebook.react.bridge.queue; -import android.os.Looper; - -import com.facebook.common.logging.FLog; import com.facebook.proguard.annotations.DoNotStrip; -import com.facebook.react.bridge.AssertionException; -import com.facebook.react.bridge.SoftAssertions; -import com.facebook.react.common.ReactConstants; -import com.facebook.react.common.futures.SimpleSettableFuture; /** - * Encapsulates a Thread that has a {@link Looper} running on it that can accept Runnables. + * Encapsulates a Thread that can accept Runnables. */ @DoNotStrip -public class MessageQueueThread { - - private final String mName; - private final Looper mLooper; - private final MessageQueueThreadHandler mHandler; - private final String mAssertionErrorMessage; - private volatile boolean mIsFinished = false; - - private MessageQueueThread( - String name, - Looper looper, - QueueThreadExceptionHandler exceptionHandler) { - mName = name; - mLooper = looper; - mHandler = new MessageQueueThreadHandler(looper, exceptionHandler); - mAssertionErrorMessage = "Expected to be called from the '" + getName() + "' thread!"; - } - +public interface MessageQueueThread { /** * Runs the given Runnable on this Thread. It will be submitted to the end of the event queue even * if it is being submitted from the same queue Thread. */ @DoNotStrip - public void runOnQueue(Runnable runnable) { - if (mIsFinished) { - FLog.w( - ReactConstants.TAG, - "Tried to enqueue runnable on already finished thread: '" + getName() + - "... dropping Runnable."); - } - mHandler.post(runnable); - } + void runOnQueue(Runnable runnable); /** * @return whether the current Thread is also the Thread associated with this MessageQueueThread. */ - public boolean isOnThread() { - return mLooper.getThread() == Thread.currentThread(); - } + boolean isOnThread(); /** * Asserts {@link #isOnThread()}, throwing a {@link AssertionException} (NOT an * {@link AssertionError}) if the assertion fails. */ - public void assertIsOnThread() { - SoftAssertions.assertCondition(isOnThread(), mAssertionErrorMessage); - } - - /** - * Quits this queue's Looper. If that Looper was running on a different Thread than the current - * Thread, also waits for the last message being processed to finish and the Thread to die. - */ - public void quitSynchronous() { - mIsFinished = true; - mLooper.quit(); - if (mLooper.getThread() != Thread.currentThread()) { - try { - mLooper.getThread().join(); - } catch (InterruptedException e) { - throw new RuntimeException("Got interrupted waiting to join thread " + mName); - } - } - } - - public Looper getLooper() { - return mLooper; - } - - public String getName() { - return mName; - } - - public static MessageQueueThread create( - MessageQueueThreadSpec spec, - QueueThreadExceptionHandler exceptionHandler) { - switch (spec.getThreadType()) { - case MAIN_UI: - return createForMainThread(spec.getName(), exceptionHandler); - case NEW_BACKGROUND: - return startNewBackgroundThread(spec.getName(), exceptionHandler); - default: - throw new RuntimeException("Unknown thread type: " + spec.getThreadType()); - } - } - - /** - * @return a MessageQueueThread corresponding to Android's main UI thread. - */ - private static MessageQueueThread createForMainThread( - String name, - QueueThreadExceptionHandler exceptionHandler) { - Looper mainLooper = Looper.getMainLooper(); - return new MessageQueueThread(name, mainLooper, exceptionHandler); - } - - /** - * Creates and starts a new MessageQueueThread encapsulating a new Thread with a new Looper - * running on it. Give it a name for easier debugging. When this method exits, the new - * MessageQueueThread is ready to receive events. - */ - private static MessageQueueThread startNewBackgroundThread( - String name, - QueueThreadExceptionHandler exceptionHandler) { - final SimpleSettableFuture simpleSettableFuture = new SimpleSettableFuture<>(); - Thread bgThread = new Thread( - new Runnable() { - @Override - public void run() { - Looper.prepare(); - - simpleSettableFuture.set(Looper.myLooper()); - - Looper.loop(); - } - }, "mqt_" + name); - bgThread.start(); - - return new MessageQueueThread(name, simpleSettableFuture.get(5000), exceptionHandler); - } + void assertIsOnThread(); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/queue/MessageQueueThreadImpl.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/queue/MessageQueueThreadImpl.java new file mode 100644 index 000000000..5453e5355 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/queue/MessageQueueThreadImpl.java @@ -0,0 +1,144 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.bridge.queue; + +import android.os.Looper; + +import com.facebook.common.logging.FLog; +import com.facebook.proguard.annotations.DoNotStrip; +import com.facebook.react.bridge.AssertionException; +import com.facebook.react.bridge.SoftAssertions; +import com.facebook.react.common.ReactConstants; +import com.facebook.react.common.futures.SimpleSettableFuture; + +/** + * Encapsulates a Thread that has a {@link Looper} running on it that can accept Runnables. + */ +@DoNotStrip +/* package */ class MessageQueueThreadImpl implements MessageQueueThread { + + private final String mName; + private final Looper mLooper; + private final MessageQueueThreadHandler mHandler; + private final String mAssertionErrorMessage; + private volatile boolean mIsFinished = false; + + private MessageQueueThreadImpl( + String name, + Looper looper, + QueueThreadExceptionHandler exceptionHandler) { + mName = name; + mLooper = looper; + mHandler = new MessageQueueThreadHandler(looper, exceptionHandler); + mAssertionErrorMessage = "Expected to be called from the '" + getName() + "' thread!"; + } + + /** + * Runs the given Runnable on this Thread. It will be submitted to the end of the event queue even + * if it is being submitted from the same queue Thread. + */ + @DoNotStrip + public void runOnQueue(Runnable runnable) { + if (mIsFinished) { + FLog.w( + ReactConstants.TAG, + "Tried to enqueue runnable on already finished thread: '" + getName() + + "... dropping Runnable."); + } + mHandler.post(runnable); + } + + /** + * @return whether the current Thread is also the Thread associated with this MessageQueueThread. + */ + public boolean isOnThread() { + return mLooper.getThread() == Thread.currentThread(); + } + + /** + * Asserts {@link #isOnThread()}, throwing a {@link AssertionException} (NOT an + * {@link AssertionError}) if the assertion fails. + */ + public void assertIsOnThread() { + SoftAssertions.assertCondition(isOnThread(), mAssertionErrorMessage); + } + + /** + * Quits this queue's Looper. If that Looper was running on a different Thread than the current + * Thread, also waits for the last message being processed to finish and the Thread to die. + */ + public void quitSynchronous() { + mIsFinished = true; + mLooper.quit(); + if (mLooper.getThread() != Thread.currentThread()) { + try { + mLooper.getThread().join(); + } catch (InterruptedException e) { + throw new RuntimeException("Got interrupted waiting to join thread " + mName); + } + } + } + + public Looper getLooper() { + return mLooper; + } + + public String getName() { + return mName; + } + + public static MessageQueueThreadImpl create( + MessageQueueThreadSpec spec, + QueueThreadExceptionHandler exceptionHandler) { + switch (spec.getThreadType()) { + case MAIN_UI: + return createForMainThread(spec.getName(), exceptionHandler); + case NEW_BACKGROUND: + return startNewBackgroundThread(spec.getName(), exceptionHandler); + default: + throw new RuntimeException("Unknown thread type: " + spec.getThreadType()); + } + } + + /** + * @return a MessageQueueThreadImpl corresponding to Android's main UI thread. + */ + private static MessageQueueThreadImpl createForMainThread( + String name, + QueueThreadExceptionHandler exceptionHandler) { + Looper mainLooper = Looper.getMainLooper(); + return new MessageQueueThreadImpl(name, mainLooper, exceptionHandler); + } + + /** + * Creates and starts a new MessageQueueThreadImpl encapsulating a new Thread with a new Looper + * running on it. Give it a name for easier debugging. When this method exits, the new + * MessageQueueThreadImpl is ready to receive events. + */ + private static MessageQueueThreadImpl startNewBackgroundThread( + String name, + QueueThreadExceptionHandler exceptionHandler) { + final SimpleSettableFuture simpleSettableFuture = new SimpleSettableFuture<>(); + Thread bgThread = new Thread( + new Runnable() { + @Override + public void run() { + Looper.prepare(); + + simpleSettableFuture.set(Looper.myLooper()); + + Looper.loop(); + } + }, "mqt_" + name); + bgThread.start(); + + return new MessageQueueThreadImpl(name, simpleSettableFuture.get(5000), exceptionHandler); + } +}