Release React Native for Android

This is an early release and there are several things that are known
not to work if you're porting your iOS app to Android.

See the Known Issues guide on the website.

We will work with the community to reach platform parity with iOS.
This commit is contained in:
Martin Konicek
2015-09-14 18:13:39 +01:00
parent c372dab213
commit 42eb5464fd
571 changed files with 44550 additions and 116 deletions
@@ -0,0 +1,88 @@
/**
* 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 java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.facebook.react.bridge.JavaScriptModule;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
/**
* {@code CompositeReactPackage} allows to create a single package composed of views and modules
* from several other packages.
*/
public class CompositeReactPackage implements ReactPackage {
private final List<ReactPackage> mChildReactPackages = new ArrayList<>();
/**
* The order in which packages are passed matters. It may happen that a NativeModule or
* or a ViewManager exists in two or more ReactPackages. In that case the latter will win
* i.e. the latter will overwrite the former. This re-occurrence is detected by
* comparing a name of a module.
*/
public CompositeReactPackage(ReactPackage arg1, ReactPackage arg2, ReactPackage... args) {
mChildReactPackages.add(arg1);
mChildReactPackages.add(arg2);
for (ReactPackage reactPackage: args) {
mChildReactPackages.add(reactPackage);
}
}
/**
* {@inheritDoc}
*/
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
final Map<String, NativeModule> moduleMap = new HashMap<>();
for (ReactPackage reactPackage: mChildReactPackages) {
for (NativeModule nativeModule: reactPackage.createNativeModules(reactContext)) {
moduleMap.put(nativeModule.getName(), nativeModule);
}
}
return new ArrayList(moduleMap.values());
}
/**
* {@inheritDoc}
*/
@Override
public List<Class<? extends JavaScriptModule>> createJSModules() {
final Set<Class<? extends JavaScriptModule>> moduleSet = new HashSet<>();
for (ReactPackage reactPackage: mChildReactPackages) {
for (Class<? extends JavaScriptModule> jsModule: reactPackage.createJSModules()) {
moduleSet.add(jsModule);
}
}
return new ArrayList(moduleSet);
}
/**
* {@inheritDoc}
*/
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
final Map<String, ViewManager> viewManagerMap = new HashMap<>();
for (ReactPackage reactPackage: mChildReactPackages) {
for (ViewManager viewManager: reactPackage.createViewManagers(reactContext)) {
viewManagerMap.put(viewManager.getName(), viewManager);
}
}
return new ArrayList(viewManagerMap.values());
}
}
@@ -0,0 +1,86 @@
/**
* 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 java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import com.facebook.catalyst.uimanager.debug.DebugComponentOwnershipModule;
import com.facebook.react.bridge.JavaScriptModule;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.facebook.react.modules.core.ExceptionsManagerModule;
import com.facebook.react.modules.core.JSTimersExecution;
import com.facebook.react.modules.core.Timing;
import com.facebook.react.modules.debug.AnimationsDebugModule;
import com.facebook.react.modules.debug.SourceCodeModule;
import com.facebook.react.modules.systeminfo.AndroidInfoModule;
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.react.uimanager.events.RCTEventEmitter;
/**
* Package defining core framework modules (e.g. UIManager). It should be used for modules that
* require special integration with other framework parts (e.g. with the list of packages to load
* view managers from).
*/
/* package */ class CoreModulesPackage implements ReactPackage {
private final ReactInstanceManager mReactInstanceManager;
private final DefaultHardwareBackBtnHandler mHardwareBackBtnHandler;
CoreModulesPackage(
ReactInstanceManager reactInstanceManager,
DefaultHardwareBackBtnHandler hardwareBackBtnHandler) {
mReactInstanceManager = reactInstanceManager;
mHardwareBackBtnHandler = hardwareBackBtnHandler;
}
@Override
public List<NativeModule> createNativeModules(
ReactApplicationContext catalystApplicationContext) {
return Arrays.<NativeModule>asList(
new AnimationsDebugModule(
catalystApplicationContext,
mReactInstanceManager.getDevSupportManager().getDevSettings()),
new AndroidInfoModule(),
new DeviceEventManagerModule(catalystApplicationContext, mHardwareBackBtnHandler),
new ExceptionsManagerModule(mReactInstanceManager.getDevSupportManager()),
new Timing(catalystApplicationContext),
new SourceCodeModule(
mReactInstanceManager.getDevSupportManager().getSourceUrl(),
mReactInstanceManager.getDevSupportManager().getSourceMapUrl()),
new UIManagerModule(
catalystApplicationContext,
mReactInstanceManager.createAllViewManagers(catalystApplicationContext)),
new DebugComponentOwnershipModule(catalystApplicationContext));
}
@Override
public List<Class<? extends JavaScriptModule>> createJSModules() {
return Arrays.asList(
DeviceEventManagerModule.RCTDeviceEventEmitter.class,
JSTimersExecution.class,
RCTEventEmitter.class,
AppRegistry.class,
ReactNative.class,
DebugComponentOwnershipModule.RCTDebugComponentOwnership.class);
}
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return new ArrayList<>(0);
}
}
@@ -0,0 +1,27 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react;
/**
* Lifecycle state for an Activity. The state right after pause and right before resume are the
* basically the same so this enum is in terms of the forward lifecycle progression (onResume, etc).
* Eventually, if necessary, it could contain something like:
*
* BEFORE_CREATE,
* CREATED,
* VIEW_CREATED,
* STARTED,
* RESUMED
*/
public enum LifecycleState {
BEFORE_RESUME,
RESUMED,
}
@@ -0,0 +1,564 @@
/**
* 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.Application;
import android.content.Context;
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.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.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 ReactInstanceManager} 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 tp 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}.
*/
public class ReactInstanceManager {
/* should only be accessed from main thread (UI thread) */
private final List<ReactRootView> mAttachedRootViews = new ArrayList<>();
private LifecycleState mLifecycleState;
/* accessed from any thread */
private final @Nullable String mBundleAssetName; /* name of JS bundle file in assets folder */
private final @Nullable String mJSMainModuleName; /* path to JS bundle root on packager server */
private final List<ReactPackage> 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 final ReactInstanceDevCommandsHandler mDevInterface =
new ReactInstanceDevCommandsHandler() {
@Override
public void onReloadWithJSDebugger(ProxyJavaScriptExecutor proxyExecutor) {
ReactInstanceManager.this.onReloadWithJSDebugger(proxyExecutor);
}
@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 ReactInstanceManager(
Context applicationContext,
@Nullable String bundleAssetName,
@Nullable String jsMainModuleName,
List<ReactPackage> packages,
boolean useDeveloperSupport,
@Nullable NotThreadSafeBridgeIdleDebugListener bridgeIdleDebugListener,
LifecycleState initialLifecycleState) {
initializeSoLoaderIfNecessary(applicationContext);
mApplicationContext = applicationContext;
mBundleAssetName = bundleAssetName;
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);
}
/**
* 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);
}
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
* ReactInstanceManager}, 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 ReactInstanceManager}.
*/
public void onResume(DefaultHardwareBackBtnHandler defaultBackButtonImpl) {
UiThreadUtil.assertOnUiThread();
mLifecycleState = LifecycleState.RESUMED;
mDefaultBackButtonImpl = defaultBackButtonImpl;
if (mUseDeveloperSupport) {
mDevSupportManager.setDevSupportEnabled(true);
}
if (mCurrentReactContext != null) {
mCurrentReactContext.onResume();
}
}
public void onDestroy() {
UiThreadUtil.assertOnUiThread();
if (mUseDeveloperSupport) {
mDevSupportManager.setDevSupportEnabled(false);
}
if (mCurrentReactContext != null) {
mCurrentReactContext.onDestroy();
}
}
public void showDevOptionsDialog() {
UiThreadUtil.assertOnUiThread();
mDevSupportManager.showDevOptionsDialog();
}
/**
* Attach given {@param rootView} to a catalyst instance manager and start JS application using
* JS module provided by {@link ReactRootView#getJSModuleName}. 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 (mCurrentReactContext == null) {
initializeReactContext();
} else {
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.
*/
/* package */ 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
*/
/* package */ List<ViewManager> createAllViewManagers(
ReactApplicationContext catalystApplicationContext) {
List<ViewManager> allViewManagers = new ArrayList<>();
for (ReactPackage reactPackage : mPackages) {
allViewManagers.addAll(reactPackage.createViewManagers(catalystApplicationContext));
}
return allViewManagers;
}
@VisibleForTesting
public @Nullable ReactContext getCurrentReactContext() {
return mCurrentReactContext;
}
private void onReloadWithJSDebugger(ProxyJavaScriptExecutor proxyExecutor) {
recreateReactContext(
proxyExecutor,
JSBundleLoader.createRemoteDebuggerBundleLoader(
mDevSupportManager.getJSBundleURLForRemoteDebugging()));
}
private void onJSBundleLoadedFromServer() {
recreateReactContext(
new JSCJavaScriptExecutor(),
JSBundleLoader.createCachedBundleFromNetworkLoader(
mDevSupportManager.getSourceUrl(),
mDevSupportManager.getDownloadedJSBundleFile()));
}
private void initializeReactContext() {
if (mUseDeveloperSupport) {
if (mDevSupportManager.hasUpToDateJSBundleInCache()) {
// If there is a up-to-date bundle downloaded from server, always use that
onJSBundleLoadedFromServer();
return;
} else if (mBundleAssetName == null ||
!mDevSupportManager.hasBundleInAssets(mBundleAssetName)) {
// Bundle not available in assets, fetch from the server
mDevSupportManager.handleReloadJS();
return;
}
}
// Use JS file from assets
recreateReactContext(
new JSCJavaScriptExecutor(),
JSBundleLoader.createAssetLoader(
mApplicationContext.getAssets(),
mBundleAssetName));
}
private void recreateReactContext(
JavaScriptExecutor jsExecutor,
JSBundleLoader jsBundleLoader) {
UiThreadUtil.assertOnUiThread();
if (mCurrentReactContext != null) {
tearDownReactContext(mCurrentReactContext);
}
mCurrentReactContext = createReactContext(jsExecutor, jsBundleLoader);
for (ReactRootView rootView : mAttachedRootViews) {
attachMeasuredRootViewToInstance(
rootView,
mCurrentReactContext.getCatalystInstance());
}
}
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) {
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.initialize();
mDevSupportManager.onNewReactContextCreated(reactContext);
moveReactContextToCurrentLifecycleState(reactContext);
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<? extends JavaScriptModule> jsModuleClass : reactPackage.createJSModules()) {
jsModulesBuilder.add(jsModuleClass);
}
}
private void moveReactContextToCurrentLifecycleState(ReactApplicationContext reactContext) {
if (mLifecycleState == LifecycleState.RESUMED) {
reactContext.onResume();
}
}
/**
* Builder class for {@link ReactInstanceManager}
*/
public static class Builder {
private final List<ReactPackage> mPackages = new ArrayList<>();
private @Nullable String mBundleAssetName;
private @Nullable String mJSMainModuleName;
private @Nullable NotThreadSafeBridgeIdleDebugListener mBridgeIdleDebugListener;
private @Nullable Application mApplication;
private boolean mUseDeveloperSupport;
private @Nullable LifecycleState mInitialLifecycleState;
private Builder() {
}
/**
* Name of the JS budle file to be loaded from application's raw assets.
* Example: {@code "index.android.js"}
*/
public Builder setBundleAssetName(String bundleAssetName) {
mBundleAssetName = bundleAssetName;
return this;
}
/**
* Path to your app's main module on the packager server. This is used when
* reloading JS during development. All paths are relative to the root folder
* the packager is serving files from.
* Examples:
* {@code "index.android"} or
* {@code "subdirectory/index.android"}
*/
public Builder setJSMainModuleName(String jsMainModuleName) {
mJSMainModuleName = jsMainModuleName;
return this;
}
public Builder addPackage(ReactPackage reactPackage) {
mPackages.add(reactPackage);
return this;
}
public Builder setBridgeIdleDebugListener(
NotThreadSafeBridgeIdleDebugListener bridgeIdleDebugListener) {
mBridgeIdleDebugListener = bridgeIdleDebugListener;
return this;
}
/**
* Required. This must be your {@code Application} instance.
*/
public Builder setApplication(Application application) {
mApplication = application;
return this;
}
/**
* When {@code true}, developer options such as JS reloading and debugging are enabled.
* Note you still have to call {@link #showDevOptionsDialog} to show the dev menu,
* e.g. when the device Menu button is pressed.
*/
public Builder setUseDeveloperSupport(boolean useDeveloperSupport) {
mUseDeveloperSupport = useDeveloperSupport;
return this;
}
/**
* Sets the initial lifecycle state of the host. For example, if the host is already resumed at
* creation time, we wouldn't expect an onResume call until we get an onPause call.
*/
public Builder setInitialLifecycleState(LifecycleState initialLifecycleState) {
mInitialLifecycleState = initialLifecycleState;
return this;
}
/**
* Instantiates a new {@link ReactInstanceManager}.
* Before calling {@code build}, the following must be called:
* <ul>
* <li> {@link #setApplication}
* <li> {@link #setBundleAssetName} or {@link #setJSMainModuleName}
* </ul>
*/
public ReactInstanceManager build() {
Assertions.assertCondition(
mUseDeveloperSupport || mBundleAssetName != null,
"JS Bundle has to be provided in app assets when dev support is disabled");
Assertions.assertCondition(
mBundleAssetName != null || mJSMainModuleName != null,
"Either BundleAssetName or MainModuleName needs to be provided");
return new ReactInstanceManager(
Assertions.assertNotNull(
mApplication,
"Application property has not been set with this builder"),
mBundleAssetName,
mJSMainModuleName,
mPackages,
mUseDeveloperSupport,
mBridgeIdleDebugListener,
Assertions.assertNotNull(mInitialLifecycleState, "Initial lifecycle state was not set"));
}
}
}
@@ -0,0 +1,54 @@
/**
* 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 java.util.List;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.JavaScriptModule;
import com.facebook.react.uimanager.UIManagerModule;
import com.facebook.react.uimanager.ViewManager;
/**
* Main interface for providing additional capabilities to the catalyst framework by couple of
* different means:
* 1) Registering new native modules
* 2) Registering new JS modules that may be accessed from native modules or from other parts of the
* native code (requiring JS modules from the package doesn't mean it will automatically be included
* as a part of the JS bundle, so there should be a corresponding piece of code on JS side that will
* require implementation of that JS module so that it gets bundled)
* 3) Registering custom native views (view managers) and custom event types
* 4) Registering natively packaged assets/resources (e.g. images) exposed to JS
*
* TODO(6788500, 6788507): Implement support for adding custom views, events and resources
*/
public interface ReactPackage {
/**
* @param reactContext react application context that can be used to create modules
* @return list of native modules to register with the newly created catalyst instance
*/
List<NativeModule> createNativeModules(ReactApplicationContext reactContext);
/**
* @return list of JS modules to register with the newly created catalyst instance.
*
* IMPORTANT: Note that only modules that needs to be accessible from the native code should be
* listed here. Also listing a native module here doesn't imply that the JS implementation of it
* will be automatically included in the JS bundle.
*/
List<Class<? extends JavaScriptModule>> createJSModules();
/**
* @return a list of view managers that should be registered with {@link UIManagerModule}
*/
List<ViewManager> createViewManagers(ReactApplicationContext reactContext);
}
@@ -0,0 +1,374 @@
/**
* 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 android.content.Context;
import android.graphics.Rect;
import android.os.Bundle;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import com.facebook.common.logging.FLog;
import com.facebook.infer.annotation.Assertions;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.UiThreadUtil;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.common.ReactConstants;
import com.facebook.react.common.annotations.VisibleForTesting;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.facebook.react.uimanager.DisplayMetricsHolder;
import com.facebook.react.uimanager.PixelUtil;
import com.facebook.react.uimanager.RootView;
import com.facebook.react.uimanager.SizeMonitoringFrameLayout;
import com.facebook.react.uimanager.TouchTargetHelper;
import com.facebook.react.uimanager.UIManagerModule;
import com.facebook.react.uimanager.events.EventDispatcher;
import com.facebook.react.uimanager.events.TouchEvent;
import com.facebook.react.uimanager.events.TouchEventType;
/**
* Default root view for catalyst apps. Provides the ability to listen for size changes so that a UI
* manager can re-layout its elements.
* It is also responsible for handling touch events passed to any of it's child view's and sending
* those events to JS via RCTEventEmitter module. This view is overriding
* {@link ViewGroup#onInterceptTouchEvent} method in order to be notified about the events for all
* of it's children and it's also overriding {@link ViewGroup#requestDisallowInterceptTouchEvent}
* to make sure that {@link ViewGroup#onInterceptTouchEvent} will get events even when some child
* view start intercepting it. In case when no child view is interested in handling some particular
* touch event this view's {@link View#onTouchEvent} will still return true in order to be notified
* about all subsequent touch events related to that gesture (in case when JS code want to handle
* that gesture).
*/
public class ReactRootView extends SizeMonitoringFrameLayout implements RootView {
private final KeyboardListener mKeyboardListener = new KeyboardListener();
private @Nullable ReactInstanceManager mReactInstanceManager;
private @Nullable String mJSModuleName;
private @Nullable Bundle mLaunchOptions;
private int mTargetTag = -1;
private boolean mChildIsHandlingNativeGesture = false;
private boolean mWasMeasured = false;
private boolean mAttachScheduled = false;
private boolean mIsAttachedToWindow = false;
private boolean mIsAttachedToInstance = false;
public ReactRootView(Context context) {
super(context);
}
public ReactRootView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ReactRootView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if (widthMode == MeasureSpec.UNSPECIFIED || heightMode == MeasureSpec.UNSPECIFIED) {
throw new IllegalStateException(
"The root catalyst view must have a width and height given to it by it's parent view. " +
"You can do this by specifying MATCH_PARENT or explicit width and height in the layout.");
}
setMeasuredDimension(
MeasureSpec.getSize(widthMeasureSpec),
MeasureSpec.getSize(heightMeasureSpec));
mWasMeasured = true;
if (mAttachScheduled && mReactInstanceManager != null && mIsAttachedToWindow) {
// Scheduled from {@link #startReactApplication} call in case when the view measurements are
// not available
mAttachScheduled = false;
// Enqueue it to UIThread not to block onMeasure waiting for the catalyst instance creation
UiThreadUtil.runOnUiThread(new Runnable() {
@Override
public void run() {
Assertions.assertNotNull(mReactInstanceManager)
.attachMeasuredRootView(ReactRootView.this);
mIsAttachedToInstance = true;
getViewTreeObserver().addOnGlobalLayoutListener(mKeyboardListener);
}
});
}
}
/**
* Main catalyst view is responsible for collecting and sending touch events to JS. This method
* reacts for an incoming android native touch events ({@link MotionEvent}) and calls into
* {@link com.facebook.react.uimanager.events.EventDispatcher} when appropriate.
* It uses {@link com.facebook.react.uimanager.TouchTargetManagerHelper#findTouchTargetView}
* helper method for figuring out a react view ID in the case of ACTION_DOWN
* event (when the gesture starts).
*/
private void handleTouchEvent(MotionEvent ev) {
if (mReactInstanceManager == null || !mIsAttachedToInstance ||
mReactInstanceManager.getCurrentReactContext() == null) {
FLog.w(
ReactConstants.TAG,
"Unable to handle touch in JS as the catalyst instance has not been attached");
return;
}
int action = ev.getAction() & MotionEvent.ACTION_MASK;
ReactContext reactContext = mReactInstanceManager.getCurrentReactContext();
EventDispatcher eventDispatcher = reactContext.getNativeModule(UIManagerModule.class)
.getEventDispatcher();
if (action == MotionEvent.ACTION_DOWN) {
if (mTargetTag != -1) {
FLog.e(
ReactConstants.TAG,
"Got DOWN touch before receiving UP or CANCEL from last gesture");
}
// First event for this gesture. We expect tag to be set to -1, and we use helper method
// {@link #findTargetTagForTouch} to find react view ID that will be responsible for handling
// this gesture
mChildIsHandlingNativeGesture = false;
mTargetTag = TouchTargetHelper.findTargetTagForTouch(ev.getRawY(), ev.getRawX(), this);
eventDispatcher.dispatchEvent(new TouchEvent(mTargetTag, TouchEventType.START, ev));
} else if (mChildIsHandlingNativeGesture) {
// If the touch was intercepted by a child, we've already sent a cancel event to JS for this
// gesture, so we shouldn't send any more touches related to it.
return;
} else if (mTargetTag == -1) {
// All the subsequent action types are expected to be called after ACTION_DOWN thus target
// is supposed to be set for them.
FLog.e(
ReactConstants.TAG,
"Unexpected state: received touch event but didn't get starting ACTION_DOWN for this " +
"gesture before");
} else if (action == MotionEvent.ACTION_UP) {
// End of the gesture. We reset target tag to -1 and expect no further event associated with
// this gesture.
eventDispatcher.dispatchEvent(new TouchEvent(mTargetTag, TouchEventType.END, ev));
mTargetTag = -1;
} else if (action == MotionEvent.ACTION_MOVE) {
// Update pointer position for current gesture
eventDispatcher.dispatchEvent(new TouchEvent(mTargetTag, TouchEventType.MOVE, ev));
} else if (action == MotionEvent.ACTION_POINTER_DOWN) {
// New pointer goes down, this can only happen after ACTION_DOWN is sent for the first pointer
eventDispatcher.dispatchEvent(new TouchEvent(mTargetTag, TouchEventType.START, ev));
} else if (action == MotionEvent.ACTION_POINTER_UP) {
// Exactly onw of the pointers goes up
eventDispatcher.dispatchEvent(new TouchEvent(mTargetTag, TouchEventType.END, ev));
} else if (action == MotionEvent.ACTION_CANCEL) {
dispatchCancelEvent(ev);
mTargetTag = -1;
} else {
FLog.w(
ReactConstants.TAG,
"Warning : touch event was ignored. Action=" + action + " Target=" + mTargetTag);
}
}
@Override
public void onChildStartedNativeGesture(MotionEvent androidEvent) {
if (mChildIsHandlingNativeGesture) {
// This means we previously had another child start handling this native gesture and now a
// different native parent of that child has decided to intercept the touch stream and handle
// the gesture itself. Example where this can happen: HorizontalScrollView in a ScrollView.
return;
}
dispatchCancelEvent(androidEvent);
mChildIsHandlingNativeGesture = true;
mTargetTag = -1;
}
private void dispatchCancelEvent(MotionEvent androidEvent) {
// This means the gesture has already ended, via some other CANCEL or UP event. This is not
// expected to happen very often as it would mean some child View has decided to intercept the
// touch stream and start a native gesture only upon receiving the UP/CANCEL event.
if (mTargetTag == -1) {
FLog.w(
ReactConstants.TAG,
"Can't cancel already finished gesture. Is a child View trying to start a gesture from " +
"an UP/CANCEL event?");
return;
}
EventDispatcher eventDispatcher = mReactInstanceManager.getCurrentReactContext()
.getNativeModule(UIManagerModule.class)
.getEventDispatcher();
Assertions.assertCondition(
!mChildIsHandlingNativeGesture,
"Expected to not have already sent a cancel for this gesture");
Assertions.assertNotNull(eventDispatcher).dispatchEvent(
new TouchEvent(mTargetTag, TouchEventType.CANCEL, androidEvent));
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
handleTouchEvent(ev);
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
handleTouchEvent(ev);
super.onTouchEvent(ev);
// In case when there is no children interested in handling touch event, we return true from
// the root view in order to receive subsequent events related to that gesture
return true;
}
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
// No-op - override in order to still receive events to onInterceptTouchEvent
// even when some other view disallow that
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
// No-op since UIManagerModule handles actually laying out children.
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
mIsAttachedToWindow = false;
if (mReactInstanceManager != null && !mAttachScheduled) {
mReactInstanceManager.detachRootView(this);
mIsAttachedToInstance = false;
getViewTreeObserver().removeOnGlobalLayoutListener(mKeyboardListener);
}
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
mIsAttachedToWindow = true;
// If the view re-attached and catalyst instance has been set before, we'd attach again to the
// catalyst instance (expecting measure to be called after {@link onAttachedToWindow})
if (mReactInstanceManager != null) {
mAttachScheduled = true;
}
}
/**
* {@see #startReactApplication(ReactInstanceManager, String, android.os.Bundle)}
*/
public void startReactApplication(ReactInstanceManager reactInstanceManager, String moduleName) {
startReactApplication(reactInstanceManager, moduleName, null);
}
/**
* Schedule rendering of the react component rendered by the JS application from the given JS
* module (@{param moduleName}) using provided {@param reactInstanceManager} to attach to the
* JS context of that manager. Extra parameter {@param launchOptions} can be used to pass initial
* properties for the react component.
*/
public void startReactApplication(
ReactInstanceManager reactInstanceManager,
String moduleName,
@Nullable Bundle launchOptions) {
// TODO(6788889): Use POJO instead of bundle here, apparently we can't just use WritableMap
// here as it may be deallocated in native after passing via JNI bridge, but we want to reuse
// it in the case of re-creating the catalyst instance
Assertions.assertCondition(
mReactInstanceManager == null,
"This root view has already " +
"been attached to a catalyst instance manager");
mReactInstanceManager = reactInstanceManager;
mJSModuleName = moduleName;
mLaunchOptions = launchOptions;
// We need to wait for the initial onMeasure, if this view has not yet been measured, we set
// mAttachScheduled flag, which will make this view startReactApplication itself to instance
// manager once onMeasure is called.
if (mWasMeasured && mIsAttachedToWindow) {
mReactInstanceManager.attachMeasuredRootView(this);
mIsAttachedToInstance = true;
getViewTreeObserver().addOnGlobalLayoutListener(mKeyboardListener);
} else {
mAttachScheduled = true;
}
}
/* package */ String getJSModuleName() {
return Assertions.assertNotNull(mJSModuleName);
}
/* package */ @Nullable Bundle getLaunchOptions() {
return mLaunchOptions;
}
/**
* Is used by unit test to setup mWasMeasured and mIsAttachedToWindow flags, that will let this
* view to be properly attached to catalyst instance by startReactApplication call
*/
@VisibleForTesting
/* package */ void simulateAttachForTesting() {
mIsAttachedToWindow = true;
mIsAttachedToInstance = true;
mWasMeasured = true;
}
private class KeyboardListener implements ViewTreeObserver.OnGlobalLayoutListener {
private int mKeyboardHeight = 0;
private final Rect mVisibleViewArea = new Rect();
@Override
public void onGlobalLayout() {
if (mReactInstanceManager == null || !mIsAttachedToInstance ||
mReactInstanceManager.getCurrentReactContext() == null) {
FLog.w(
ReactConstants.TAG,
"Unable to dispatch keyboard events in JS as the react instance has not been attached");
return;
}
getRootView().getWindowVisibleDisplayFrame(mVisibleViewArea);
final int heightDiff =
DisplayMetricsHolder.getDisplayMetrics().heightPixels - mVisibleViewArea.bottom;
if (mKeyboardHeight != heightDiff && heightDiff > 0) {
// keyboard is now showing, or the keyboard height has changed
mKeyboardHeight = heightDiff;
WritableMap params = Arguments.createMap();
WritableMap coordinates = Arguments.createMap();
coordinates.putDouble("screenY", PixelUtil.toDIPFromPixel(mVisibleViewArea.bottom));
coordinates.putDouble("screenX", PixelUtil.toDIPFromPixel(mVisibleViewArea.left));
coordinates.putDouble("width", PixelUtil.toDIPFromPixel(mVisibleViewArea.width()));
coordinates.putDouble("height", PixelUtil.toDIPFromPixel(mKeyboardHeight));
params.putMap("endCoordinates", coordinates);
sendEvent("keyboardDidShow", params);
} else if (mKeyboardHeight != 0 && heightDiff == 0) {
// keyboard is now hidden
mKeyboardHeight = heightDiff;
sendEvent("keyboardDidHide", null);
}
}
private void sendEvent(String eventName, @Nullable WritableMap params) {
if (mReactInstanceManager != null) {
mReactInstanceManager.getCurrentReactContext()
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit(eventName, params);
}
}
}
}
@@ -0,0 +1,64 @@
/**
* 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.animation;
import android.view.View;
/**
* Base class for {@link AnimationPropertyUpdater} subclasses that updates a pair of float property
* values. It helps to handle convertion from animation progress to the actual values as
* well as the quite common case when no starting value is provided.
*/
public abstract class AbstractFloatPairPropertyUpdater implements AnimationPropertyUpdater {
private final float[] mFromValues = new float[2];
private final float[] mToValues = new float[2];
private final float[] mUpdateValues = new float[2];
private boolean mFromSource;
protected AbstractFloatPairPropertyUpdater(float toFirst, float toSecond) {
mToValues[0] = toFirst;
mToValues[1] = toSecond;
mFromSource = true;
}
protected AbstractFloatPairPropertyUpdater(
float fromFirst,
float fromSecond,
float toFirst,
float toSecond) {
this(toFirst, toSecond);
mFromValues[0] = fromFirst;
mFromValues[1] = fromSecond;
mFromSource = false;
}
protected abstract void getProperty(View view, float[] returnValues);
protected abstract void setProperty(View view, float[] propertyValues);
@Override
public void prepare(View view) {
if (mFromSource) {
getProperty(view, mFromValues);
}
}
@Override
public void onUpdate(View view, float progress) {
mUpdateValues[0] = mFromValues[0] + (mToValues[0] - mFromValues[0]) * progress;
mUpdateValues[1] = mFromValues[1] + (mToValues[1] - mFromValues[1]) * progress;
setProperty(view, mUpdateValues);
}
@Override
public void onFinish(View view) {
setProperty(view, mToValues);
}
}
@@ -0,0 +1,54 @@
/**
* 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.animation;
import android.view.View;
/**
* Base class for {@link AnimationPropertyUpdater} subclasses that updates a single float property
* value. It helps to handle convertion from animation progress to the actual value as well as the
* quite common case when no starting value is provided.
*/
public abstract class AbstractSingleFloatProperyUpdater implements AnimationPropertyUpdater {
private float mFromValue, mToValue;
private boolean mFromSource;
protected AbstractSingleFloatProperyUpdater(float toValue) {
mToValue = toValue;
mFromSource = true;
}
protected AbstractSingleFloatProperyUpdater(float fromValue, float toValue) {
this(toValue);
mFromValue = fromValue;
mFromSource = false;
}
protected abstract float getProperty(View view);
protected abstract void setProperty(View view, float propertyValue);
@Override
public final void prepare(View view) {
if (mFromSource) {
mFromValue = getProperty(view);
}
}
@Override
public final void onUpdate(View view, float progress) {
setProperty(view, mFromValue + (mToValue - mFromValue) * progress);
}
@Override
public void onFinish(View view) {
setProperty(view, mToValue);
}
}
@@ -0,0 +1,107 @@
/**
* 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.animation;
import javax.annotation.Nullable;
import android.view.View;
import com.facebook.infer.annotation.Assertions;
/**
* Base class for various catalyst animation engines. Subclasses of this class should implement
* {@link #run} method which should bootstrap the animation. Then in each animation frame we expect
* animation engine to call {@link #onUpdate} with a float progress which then will be transferred
* to the underlying {@link AnimationPropertyUpdater} instance.
*
* Animation engine should support animation cancelling by monitoring the returned value of
* {@link #onUpdate}. In case of returning false, animation should be considered cancelled and
* engine should not attempt to call {@link #onUpdate} again.
*/
public abstract class Animation {
private final int mAnimationID;
private final AnimationPropertyUpdater mPropertyUpdater;
private volatile boolean mCancelled = false;
private volatile boolean mIsFinished = false;
private @Nullable AnimationListener mAnimationListener;
private @Nullable View mAnimatedView;
public Animation(int animationID, AnimationPropertyUpdater propertyUpdater) {
mAnimationID = animationID;
mPropertyUpdater = propertyUpdater;
}
public void setAnimationListener(AnimationListener animationListener) {
mAnimationListener = animationListener;
}
public final void start(View view) {
mAnimatedView = view;
mPropertyUpdater.prepare(view);
run();
}
public abstract void run();
/**
* Animation engine should call this method for every animation frame passing animation progress
* value as a parameter. Animation progress should be within the range 0..1 (the exception here
* would be a spring animation engine which may slightly exceed start and end progress values).
*
* This method will return false if the animation has been cancelled. In that case animation
* engine should not attempt to call this method again. Otherwise this method will return true
*/
protected final boolean onUpdate(float value) {
Assertions.assertCondition(!mIsFinished, "Animation must not already be finished!");
if (!mCancelled) {
mPropertyUpdater.onUpdate(Assertions.assertNotNull(mAnimatedView), value);
}
return !mCancelled;
}
/**
* Animation engine should call this method when the animation is finished. Should be called only
* once
*/
protected final void finish() {
Assertions.assertCondition(!mIsFinished, "Animation must not already be finished!");
mIsFinished = true;
if (!mCancelled) {
if (mAnimatedView != null) {
mPropertyUpdater.onFinish(mAnimatedView);
}
if (mAnimationListener != null) {
mAnimationListener.onFinished();
}
}
}
/**
* Cancels the animation.
*
* It is possible for this to be called after finish() and should handle that gracefully.
*/
public final void cancel() {
if (mIsFinished || mCancelled) {
// If we were already finished, ignore
return;
}
mCancelled = true;
if (mAnimationListener != null) {
mAnimationListener.onCancel();
}
}
public int getAnimationID() {
return mAnimationID;
}
}
@@ -0,0 +1,27 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.animation;
/**
* Interface for getting animation lifecycle updates. It is guaranteed that for a given animation,
* only one of onFinished and onCancel will be called, and it will be called exactly once.
*/
public interface AnimationListener {
/**
* Called once animation is finished
*/
public void onFinished();
/**
* Called in case when animation was cancelled
*/
public void onCancel();
}
@@ -0,0 +1,46 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.animation;
import android.view.View;
/**
* Interface used to update particular property types during animation. While animation is in
* progress {@link Animation} instance will call {@link #onUpdate} several times with a value
* representing animation progress. Normally value will be from 0..1 range, but for spring animation
* it can slightly exceed that limit due to bounce effect at the start/end of animation.
*/
public interface AnimationPropertyUpdater {
/**
* This method will be called before animation starts.
*
* @param view view that will be animated
*/
public void prepare(View view);
/**
* This method will be called for each animation frame
*
* @param view view to update property
* @param progress animation progress from 0..1 range (may slightly exceed that limit in case of
* spring engine) retrieved from {@link Animation} engine.
*/
public void onUpdate(View view, float progress);
/**
* This method will be called at the end of animation. It should be used to set the final values
* for animated properties in order to avoid floating point inacurracy calculated in
* {@link #onUpdate} by passing value close to 1.0 or in a case some frames got dropped.
*
* @param view view to update property
*/
public void onFinish(View view);
}
@@ -0,0 +1,43 @@
/**
* 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.animation;
import android.util.SparseArray;
import com.facebook.react.bridge.UiThreadUtil;
/**
* Coordinates catalyst animations driven by {@link UIManagerModule} and
* {@link AnimationManagerModule}
*/
public class AnimationRegistry {
private final SparseArray<Animation> mRegistry = new SparseArray<Animation>();
public void registerAnimation(Animation animation) {
UiThreadUtil.assertOnUiThread();
mRegistry.put(animation.getAnimationID(), animation);
}
public Animation getAnimation(int animationID) {
UiThreadUtil.assertOnUiThread();
return mRegistry.get(animationID);
}
public Animation removeAnimation(int animationID) {
UiThreadUtil.assertOnUiThread();
Animation animation = mRegistry.get(animationID);
if (animation != null) {
mRegistry.delete(animationID);
}
return animation;
}
}
@@ -0,0 +1,28 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.animation;
/**
* Ignores duration and immediately jump to the end of animation. This is a temporal solution for
* the lack of real animation engines implemented.
*/
public class ImmediateAnimation extends Animation {
public ImmediateAnimation(int animationID, AnimationPropertyUpdater propertyUpdater) {
super(animationID, propertyUpdater);
}
@Override
public void run() {
onUpdate(1.0f);
finish();
}
}
@@ -0,0 +1,30 @@
/**
* 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.animation;
import android.view.View;
/**
* Empty {@link AnimationPropertyUpdater} that can be used as a stub for unsupported property types
*/
public class NoopAnimationPropertyUpdater implements AnimationPropertyUpdater {
@Override
public void prepare(View view) {
}
@Override
public void onUpdate(View view, float value) {
}
@Override
public void onFinish(View view) {
}
}
@@ -0,0 +1,36 @@
/**
* 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.animation;
import android.view.View;
/**
* Subclass of {@link AnimationPropertyUpdater} for animating view's opacity
*/
public class OpacityAnimationPropertyUpdater extends AbstractSingleFloatProperyUpdater {
public OpacityAnimationPropertyUpdater(float toOpacity) {
super(toOpacity);
}
public OpacityAnimationPropertyUpdater(float fromOpacity, float toOpacity) {
super(fromOpacity, toOpacity);
}
@Override
protected float getProperty(View view) {
return view.getAlpha();
}
@Override
protected void setProperty(View view, float propertyValue) {
view.setAlpha(propertyValue);
}
}
@@ -0,0 +1,42 @@
/**
* 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.animation;
import android.view.View;
/**
* Subclass of {@link AnimationPropertyUpdater} for animating center position of a view
*/
public class PositionAnimationPairPropertyUpdater extends AbstractFloatPairPropertyUpdater {
public PositionAnimationPairPropertyUpdater(float toFirst, float toSecond) {
super(toFirst, toSecond);
}
public PositionAnimationPairPropertyUpdater(
float fromFirst,
float fromSecond,
float toFirst,
float toSecond) {
super(fromFirst, fromSecond, toFirst, toSecond);
}
@Override
protected void getProperty(View view, float[] returnValues) {
returnValues[0] = view.getX() + 0.5f * view.getWidth();
returnValues[1] = view.getY() + 0.5f * view.getHeight();
}
@Override
protected void setProperty(View view, float[] propertyValues) {
view.setX(propertyValues[0] - 0.5f * view.getWidth());
view.setY(propertyValues[1] - 0.5f * view.getHeight());
}
}
@@ -0,0 +1,32 @@
/**
* 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.animation;
import android.view.View;
/**
* Subclass of {@link AnimationPropertyUpdater} for animating view's rotation
*/
public class RotationAnimationPropertyUpdater extends AbstractSingleFloatProperyUpdater {
public RotationAnimationPropertyUpdater(float toValue) {
super(toValue);
}
@Override
protected float getProperty(View view) {
return view.getRotation();
}
@Override
protected void setProperty(View view, float propertyValue) {
view.setRotation((float) Math.toDegrees(propertyValue));
}
}
@@ -0,0 +1,36 @@
/**
* 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.animation;
import android.view.View;
/**
* Subclass of {@link AnimationPropertyUpdater} for animating view's X scale
*/
public class ScaleXAnimationPropertyUpdater extends AbstractSingleFloatProperyUpdater {
public ScaleXAnimationPropertyUpdater(float toValue) {
super(toValue);
}
public ScaleXAnimationPropertyUpdater(float fromValue, float toValue) {
super(fromValue, toValue);
}
@Override
protected float getProperty(View view) {
return view.getScaleX();
}
@Override
protected void setProperty(View view, float propertyValue) {
view.setScaleX(propertyValue);
}
}
@@ -0,0 +1,42 @@
/**
* 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.animation;
import android.view.View;
/**
* Subclass of {@link AnimationPropertyUpdater} for animating view's X and Y scale
*/
public class ScaleXYAnimationPairPropertyUpdater extends AbstractFloatPairPropertyUpdater {
public ScaleXYAnimationPairPropertyUpdater(float toFirst, float toSecond) {
super(toFirst, toSecond);
}
public ScaleXYAnimationPairPropertyUpdater(
float fromFirst,
float fromSecond,
float toFirst,
float toSecond) {
super(fromFirst, fromSecond, toFirst, toSecond);
}
@Override
protected void getProperty(View view, float[] returnValues) {
returnValues[0] = view.getScaleX();
returnValues[1] = view.getScaleY();
}
@Override
protected void setProperty(View view, float[] propertyValues) {
view.setScaleX(propertyValues[0]);
view.setScaleY(propertyValues[1]);
}
}
@@ -0,0 +1,36 @@
/**
* 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.animation;
import android.view.View;
/**
* Subclass of {@link AnimationPropertyUpdater} for animating view's Y scale
*/
public class ScaleYAnimationPropertyUpdater extends AbstractSingleFloatProperyUpdater {
public ScaleYAnimationPropertyUpdater(float toValue) {
super(toValue);
}
public ScaleYAnimationPropertyUpdater(float fromValue, float toValue) {
super(fromValue, toValue);
}
@Override
protected float getProperty(View view) {
return view.getScaleY();
}
@Override
protected void setProperty(View view, float propertyValue) {
view.setScaleY(propertyValue);
}
}
@@ -0,0 +1,142 @@
/**
* 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 android.os.Bundle;
public class Arguments {
/**
* This method should be used when you need to stub out creating NativeArrays in unit tests.
*/
public static WritableArray createArray() {
return new WritableNativeArray();
}
/**
* This method should be used when you need to stub out creating NativeMaps in unit tests.
*/
public static WritableMap createMap() {
return new WritableNativeMap();
}
public static WritableNativeArray fromJavaArgs(Object[] args) {
WritableNativeArray arguments = new WritableNativeArray();
for (int i = 0; i < args.length; i++) {
Object argument = args[i];
if (argument == null) {
arguments.pushNull();
continue;
}
Class argumentClass = argument.getClass();
if (argumentClass == Boolean.class) {
arguments.pushBoolean(((Boolean) argument).booleanValue());
} else if (argumentClass == Integer.class) {
arguments.pushDouble(((Integer) argument).doubleValue());
} else if (argumentClass == Double.class) {
arguments.pushDouble(((Double) argument).doubleValue());
} else if (argumentClass == Float.class) {
arguments.pushDouble(((Float) argument).doubleValue());
} else if (argumentClass == String.class) {
arguments.pushString(argument.toString());
} else if (argumentClass == WritableNativeMap.class) {
arguments.pushMap((WritableNativeMap) argument);
} else if (argumentClass == WritableNativeArray.class) {
arguments.pushArray((WritableNativeArray) argument);
} else {
throw new RuntimeException("Cannot convert argument of type " + argumentClass);
}
}
return arguments;
}
/**
* Convert an array to a {@link WritableArray}.
*
* @param array the array to convert. Supported types are: {@code String[]}, {@code Bundle[]},
* {@code int[]}, {@code float[]}, {@code double[]}, {@code boolean[]}.
*
* @return the converted {@link WritableArray}
* @throws IllegalArgumentException if the passed object is none of the above types
*/
public static WritableArray fromArray(Object array) {
WritableArray catalystArray = createArray();
if (array instanceof String[]) {
for (String v: (String[]) array) {
catalystArray.pushString(v);
}
} else if (array instanceof Bundle[]) {
for (Bundle v: (Bundle[]) array) {
catalystArray.pushMap(fromBundle(v));
}
} else if (array instanceof int[]) {
for (int v: (int[]) array) {
catalystArray.pushInt(v);
}
} else if (array instanceof float[]) {
for (float v: (float[]) array) {
catalystArray.pushDouble(v);
}
} else if (array instanceof double[]) {
for (double v: (double[]) array) {
catalystArray.pushDouble(v);
}
} else if (array instanceof boolean[]) {
for (boolean v: (boolean[]) array) {
catalystArray.pushBoolean(v);
}
} else {
throw new IllegalArgumentException("Unknown array type " + array.getClass());
}
return catalystArray;
}
/**
* Convert a {@link Bundle} to a {@link WritableMap}. Supported key types in the bundle
* are:
*
* <ul>
* <li>primitive types: int, float, double, boolean</li>
* <li>arrays supported by {@link #fromArray(Object)}</li>
* <li>{@link Bundle} objects that are recursively converted to maps</li>
* </ul>
*
* @param bundle the {@link Bundle} to convert
* @return the converted {@link WritableMap}
* @throws IllegalArgumentException if there are keys of unsupported types
*/
public static WritableMap fromBundle(Bundle bundle) {
WritableMap map = createMap();
for (String key: bundle.keySet()) {
Object value = bundle.get(key);
if (value == null) {
map.putNull(key);
} else if (value.getClass().isArray()) {
map.putArray(key, fromArray(value));
} else if (value instanceof String) {
map.putString(key, (String) value);
} else if (value instanceof Number) {
if (value instanceof Integer) {
map.putInt(key, (Integer) value);
} else {
map.putDouble(key, ((Number) value).doubleValue());
}
} else if (value instanceof Boolean) {
map.putBoolean(key, (Boolean) value);
} else if (value instanceof Bundle) {
map.putMap(key, fromBundle((Bundle) value));
} else {
throw new IllegalArgumentException("Could not convert " + value.getClass());
}
}
return map;
}
}
@@ -0,0 +1,22 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.bridge;
/**
* Like {@link AssertionError} but extends RuntimeException so that it may be caught by a
* {@link NativeModuleCallExceptionHandler}. See that class for more details. Used in
* conjunction with {@link SoftAssertions}.
*/
public class AssertionException extends RuntimeException {
public AssertionException(String detailMessage) {
super(detailMessage);
}
}
@@ -0,0 +1,181 @@
/**
* 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 com.fasterxml.jackson.core.JsonGenerator;
import com.facebook.systrace.Systrace;
import javax.annotation.Nullable;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
/**
* Base class for Catalyst native modules whose implementations are written in Java. Default
* implementations for {@link #initialize} and {@link #onCatalystInstanceDestroy} are provided for
* convenience. Subclasses which override these don't need to call {@code super} in case of
* overriding those methods as implementation of those methods is empty.
*
* BaseJavaModules can be linked to Fragments' lifecycle events, {@link CatalystInstance} creation
* and destruction, by being called on the appropriate method when a life cycle event occurs.
*
* Native methods can be exposed to JS with {@link ReactMethod} annotation. Those methods may
* only use limited number of types for their arguments:
* 1/ primitives (boolean, int, float, double
* 2/ {@link String} mapped from JS string
* 3/ {@link ReadableArray} mapped from JS Array
* 4/ {@link ReadableMap} mapped from JS Object
* 5/ {@link Callback} mapped from js function and can be used only as a last parameter or in the
* case when it express success & error callback pair as two last arguments respecively.
*
* All methods exposed as native to JS with {@link ReactMethod} annotation must return
* {@code void}.
*
* Please note that it is not allowed to have multiple methods annotated with {@link ReactMethod}
* with the same name.
*/
public abstract class BaseJavaModule implements NativeModule {
private class JavaMethod implements NativeMethod {
private Method method;
public JavaMethod(Method method) {
this.method = method;
}
@Override
public void invoke(CatalystInstance catalystInstance, ReadableNativeArray parameters) {
Systrace.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "callJavaModuleMethod");
try {
Class[] types = method.getParameterTypes();
if (types.length != parameters.size()) {
throw new NativeArgumentsParseException(
BaseJavaModule.this.getName() + "." + method.getName() + " got " + parameters.size() +
" arguments, expected " + types.length);
}
Object[] arguments = new Object[types.length];
int i = 0;
try {
for (; i < types.length; i++) {
Class argumentClass = types[i];
if (argumentClass == Boolean.class || argumentClass == boolean.class) {
arguments[i] = Boolean.valueOf(parameters.getBoolean(i));
} else if (argumentClass == Integer.class || argumentClass == int.class) {
arguments[i] = Integer.valueOf((int) parameters.getDouble(i));
} else if (argumentClass == Double.class || argumentClass == double.class) {
arguments[i] = Double.valueOf(parameters.getDouble(i));
} else if (argumentClass == Float.class || argumentClass == float.class) {
arguments[i] = Float.valueOf((float) parameters.getDouble(i));
} else if (argumentClass == String.class) {
arguments[i] = parameters.getString(i);
} else if (argumentClass == Callback.class) {
if (parameters.isNull(i)) {
arguments[i] = null;
} else {
int id = (int) parameters.getDouble(i);
arguments[i] = new CallbackImpl(catalystInstance, id);
}
} else if (argumentClass == ReadableMap.class) {
arguments[i] = parameters.getMap(i);
} else if (argumentClass == ReadableArray.class) {
arguments[i] = parameters.getArray(i);
} else {
throw new RuntimeException(
"Got unknown argument class: " + argumentClass.getSimpleName());
}
}
} catch (UnexpectedNativeTypeException e) {
throw new NativeArgumentsParseException(
e.getMessage() + " (constructing arguments for " + BaseJavaModule.this.getName() +
"." + method.getName() + " at argument index " + i + ")",
e);
}
try {
method.invoke(BaseJavaModule.this, arguments);
} catch (IllegalArgumentException ie) {
throw new RuntimeException(
"Could not invoke " + BaseJavaModule.this.getName() + "." + method.getName(), ie);
} catch (IllegalAccessException iae) {
throw new RuntimeException(
"Could not invoke " + BaseJavaModule.this.getName() + "." + method.getName(), iae);
} catch (InvocationTargetException ite) {
// Exceptions thrown from native module calls end up wrapped in InvocationTargetException
// which just make traces harder to read and bump out useful information
if (ite.getCause() instanceof RuntimeException) {
throw (RuntimeException) ite.getCause();
}
throw new RuntimeException(
"Could not invoke " + BaseJavaModule.this.getName() + "." + method.getName(), ite);
}
} finally {
Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE);
}
}
}
@Override
public final Map<String, NativeMethod> getMethods() {
Map<String, NativeMethod> methods = new HashMap<String, NativeMethod>();
Method[] targetMethods = getClass().getDeclaredMethods();
for (int i = 0; i < targetMethods.length; i++) {
Method targetMethod = targetMethods[i];
if (targetMethod.getAnnotation(ReactMethod.class) != null) {
String methodName = targetMethod.getName();
if (methods.containsKey(methodName)) {
// We do not support method overloading since js sees a function as an object regardless
// of number of params.
throw new IllegalArgumentException(
"Java Module " + getName() + " method name already registered: " + methodName);
}
methods.put(methodName, new JavaMethod(targetMethod));
}
}
return methods;
}
/**
* @return a map of constants this module exports to JS. Supports JSON types.
*/
public @Nullable Map<String, Object> getConstants() {
return null;
}
@Override
public final void writeConstantsField(JsonGenerator jg, String fieldName) throws IOException {
Map<String, Object> constants = getConstants();
if (constants == null || constants.isEmpty()) {
return;
}
jg.writeObjectFieldStart(fieldName);
for (Map.Entry<String, Object> constant : constants.entrySet()) {
JsonGeneratorHelper.writeObjectField(
jg,
constant.getKey(),
constant.getValue());
}
jg.writeEndObject();
}
@Override
public void initialize() {
// do nothing
}
@Override
public void onCatalystInstanceDestroy() {
// do nothing
}
}
@@ -0,0 +1,25 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.bridge;
/**
* Interface that represent javascript callback function which can be passed to the native module
* as a method parameter.
*/
public interface Callback {
/**
* Schedule javascript function execution represented by this {@link Callback} instance
*
* @param args arguments passed to javascript callback method via bridge
*/
public void invoke(Object... args);
}
@@ -0,0 +1,29 @@
/**
* 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;
/**
* Implementation of javascript callback function that use Bridge to schedule method execution
*/
public final class CallbackImpl implements Callback {
private final CatalystInstance mCatalystInstance;
private final int mCallbackId;
public CallbackImpl(CatalystInstance bridge, int callbackId) {
mCatalystInstance = bridge;
mCallbackId = callbackId;
}
@Override
public void invoke(Object... args) {
mCatalystInstance.invokeCallback(mCallbackId, Arguments.fromJavaArgs(args));
}
}
@@ -0,0 +1,419 @@
/**
* 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.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.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerator;
/**
* A higher level API on top of the asynchronous JSC bridge. This provides an
* environment allowing the invocation of JavaScript methods and lets a set of
* Java APIs be invokable from JavaScript as well.
*/
@DoNotStrip
public class CatalystInstance {
private static final int BRIDGE_SETUP_TIMEOUT_MS = 15000;
private static final AtomicInteger sNextInstanceIdForTrace = new AtomicInteger(1);
// Access from any thread
private final CatalystQueueConfiguration mCatalystQueueConfiguration;
private final CopyOnWriteArrayList<NotThreadSafeBridgeIdleDebugListener> mBridgeIdleListeners;
private final AtomicInteger mPendingJSCalls = new AtomicInteger(0);
private final String mJsPendingCallsTitleForTrace =
"pending_js_calls_instance" + sNextInstanceIdForTrace.getAndIncrement();
private volatile boolean mDestroyed = false;
// 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 @Nullable JavaScriptModuleRegistry mJSModuleRegistry;
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<NotThreadSafeBridgeIdleDebugListener>();
mJavaRegistry = registry;
mNativeModuleCallExceptionHandler = nativeModuleCallExceptionHandler;
final CountDownLatch initLatch = new CountDownLatch(1);
mCatalystQueueConfiguration.getJSQueueThread().runOnQueue(
new Runnable() {
@Override
public void run() {
initializeBridge(jsExecutor, registry, jsModulesConfig, jsBundleLoader);
mJSModuleRegistry =
new JavaScriptModuleRegistry(CatalystInstance.this, 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,
NativeModuleRegistry registry,
JavaScriptModulesConfig jsModulesConfig,
JSBundleLoader jsBundleLoader) {
mCatalystQueueConfiguration.getJSQueueThread().assertIsOnThread();
Assertions.assertCondition(mBridge == null, "initializeBridge should be called once");
mBridge = new ReactBridge(
jsExecutor,
new NativeModulesReactCallback(),
mCatalystQueueConfiguration.getNativeModulesQueueThread());
mBridge.setGlobalVariable(
"__fbBatchedBridgeConfig",
buildModulesConfigJSONProperty(registry, jsModulesConfig));
jsBundleLoader.loadScript(mBridge);
}
/* 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
/* 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, "<callback>");
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.
*/
/* 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();
}
}
// 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;
}
/**
* 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();
}
public CatalystQueueConfiguration getCatalystQueueConfiguration() {
return mCatalystQueueConfiguration;
}
@VisibleForTesting
public @Nullable
ReactBridge getBridge() {
return mBridge;
}
public <T extends JavaScriptModule> T getJSModule(Class<T> jsInterface) {
return Assertions.assertNotNull(mJSModuleRegistry).getJavaScriptModule(jsInterface);
}
public <T extends NativeModule> T getNativeModule(Class<T> nativeModuleInterface) {
return mJavaRegistry.getModule(nativeModuleInterface);
}
public Collection<NativeModule> 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.
*/
public void addBridgeIdleDebugListener(NotThreadSafeBridgeIdleDebugListener listener) {
mBridgeIdleListeners.add(listener);
}
/**
* Removes a NotThreadSafeBridgeIdleDebugListener previously added with
* {@link #addBridgeIdleDebugListener}
*/
public void removeBridgeIdleDebugListener(NotThreadSafeBridgeIdleDebugListener listener) {
mBridgeIdleListeners.remove(listener);
}
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();
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();
}
});
}
}
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));
}
}
}
@@ -0,0 +1,43 @@
/**
* 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 android.os.AsyncTask;
/**
* Abstract base for a AsyncTask that should have any RuntimeExceptions it throws
* handled by the {@link com.facebook.react.bridge.NativeModuleCallExceptionHandler} registered if
* the app is in dev mode.
*
* This class doesn't allow doInBackground to return a results. This is mostly because when this
* class was written that functionality wasn't used and it would require some extra code to make
* work correctly with caught exceptions. Don't let that stop you from adding it if you need it :)
*/
public abstract class GuardedAsyncTask<Params, Progress>
extends AsyncTask<Params, Progress, Void> {
private final ReactContext mReactContext;
protected GuardedAsyncTask(ReactContext reactContext) {
mReactContext = reactContext;
}
@Override
protected final Void doInBackground(Params... params) {
try {
doInBackgroundGuarded(params);
} catch (RuntimeException e) {
mReactContext.handleException(e);
}
return null;
}
protected abstract void doInBackgroundGuarded(Params... params);
}
@@ -0,0 +1,25 @@
/**
* 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 com.facebook.proguard.annotations.DoNotStrip;
/**
* Exception thrown by {@link ReadableMapKeySeyIterator#nextKey()} when the iterator tries
* to iterate over elements after the end of the key set.
*/
@DoNotStrip
public class InvalidIteratorException extends RuntimeException {
@DoNotStrip
public InvalidIteratorException(String msg) {
super(msg);
}
}
@@ -0,0 +1,43 @@
/**
* 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;
/**
* A special RuntimeException that should be thrown by native code if it has reached an exceptional
* state due to a, or a sequence of, bad commands.
*
* A good rule of thumb for whether a native Exception should extend this interface is 1) Can a
* developer make a change or correction in JS to keep this Exception from being thrown? 2) Is the
* app outside of this catalyst instance still in a good state to allow reloading and restarting
* this catalyst instance?
*
* Examples where this class is appropriate to throw:
* - JS tries to update a view with a tag that hasn't been created yet
* - JS tries to show a static image that isn't in resources
* - JS tries to use an unsupported view class
*
* Examples where this class **isn't** appropriate to throw:
* - Failed to write to localStorage because disk is full
* - Assertions about internal state (e.g. that child.getParent().indexOf(child) != -1)
*/
public class JSApplicationCausedNativeException extends RuntimeException {
public JSApplicationCausedNativeException(String detailMessage) {
super(detailMessage);
}
public JSApplicationCausedNativeException(
@Nullable String detailMessage,
@Nullable Throwable throwable) {
super(detailMessage, throwable);
}
}
@@ -0,0 +1,20 @@
/**
* 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;
/**
* An illegal argument Exception caused by an argument passed from JS.
*/
public class JSApplicationIllegalArgumentException extends JSApplicationCausedNativeException {
public JSApplicationIllegalArgumentException(String detailMessage) {
super(detailMessage);
}
}
@@ -0,0 +1,69 @@
/**
* 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 android.content.res.AssetManager;
/**
* A class that stores JS bundle information and allows {@link CatalystInstance} to load a correct
* bundle through {@link ReactBridge}.
*/
public abstract class JSBundleLoader {
/**
* This loader is recommended one for release version of your app. In that case local JS executor
* should be used. JS bundle will be read from assets directory in native code to save on passing
* large strings from java to native memory.
*/
public static JSBundleLoader createAssetLoader(
final AssetManager assetManager,
final String assetFileName) {
return new JSBundleLoader() {
@Override
public void loadScript(ReactBridge bridge) {
bridge.loadScriptFromAssets(assetManager, assetFileName);
}
};
}
/**
* This loader is used when bundle gets reloaded from dev server. In that case loader expect JS
* bundle to be prefetched and stored in local file. We do that to avoid passing large strings
* between java and native code and avoid allocating memory in java to fit whole JS bundle in it.
* Providing correct {@param sourceURL} of downloaded bundle is required for JS stacktraces to
* work correctly and allows for source maps to correctly symbolize those.
*/
public static JSBundleLoader createCachedBundleFromNetworkLoader(
final String sourceURL,
final String cachedFileLocation) {
return new JSBundleLoader() {
@Override
public void loadScript(ReactBridge bridge) {
bridge.loadScriptFromNetworkCached(sourceURL, cachedFileLocation);
}
};
}
/**
* This loader is used when proxy debugging is enabled. In that case there is no point in fetching
* the bundle from device as remote executor will have to do it anyway.
*/
public static JSBundleLoader createRemoteDebuggerBundleLoader(
final String sourceURL) {
return new JSBundleLoader() {
@Override
public void loadScript(ReactBridge bridge) {
bridge.loadScriptFromNetworkCached(sourceURL, null);
}
};
}
public abstract void loadScript(ReactBridge bridge);
}
@@ -0,0 +1,28 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.bridge;
import com.facebook.proguard.annotations.DoNotStrip;
import com.facebook.soloader.SoLoader;
@DoNotStrip
public class JSCJavaScriptExecutor extends JavaScriptExecutor {
static {
SoLoader.loadLibrary(ReactBridge.REACT_NATIVE_LIB);
}
public JSCJavaScriptExecutor() {
initialize();
}
private native void initialize();
}
@@ -0,0 +1,269 @@
/**
* 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.HashMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.TimeUnit;
import com.facebook.common.logging.FLog;
import com.facebook.infer.annotation.Assertions;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.Response;
import com.squareup.okhttp.ws.WebSocket;
import com.squareup.okhttp.ws.WebSocketCall;
import com.squareup.okhttp.ws.WebSocketListener;
import okio.Buffer;
import okio.BufferedSource;
/**
* A wrapper around WebSocketClient that recognizes RN debugging message format.
*/
public class JSDebuggerWebSocketClient implements WebSocketListener {
private static final String TAG = "JSDebuggerWebSocketClient";
private static final JsonFactory mJsonFactory = new JsonFactory();
public interface JSDebuggerCallback {
void onSuccess(@Nullable String response);
void onFailure(Throwable cause);
}
private @Nullable WebSocket mWebSocket;
private @Nullable OkHttpClient mHttpClient;
private @Nullable JSDebuggerCallback mConnectCallback;
private final AtomicInteger mRequestID = new AtomicInteger();
private final ConcurrentHashMap<Integer, JSDebuggerCallback> mCallbacks =
new ConcurrentHashMap<>();
public void connect(String url, JSDebuggerCallback callback) {
if (mHttpClient != null) {
throw new IllegalStateException("JSDebuggerWebSocketClient is already initialized.");
}
mConnectCallback = callback;
mHttpClient = new OkHttpClient();
mHttpClient.setConnectTimeout(10, TimeUnit.SECONDS);
mHttpClient.setWriteTimeout(10, TimeUnit.SECONDS);
// Disable timeouts for read
mHttpClient.setReadTimeout(0, TimeUnit.MINUTES);
Request request = new Request.Builder().url(url).build();
WebSocketCall call = WebSocketCall.create(mHttpClient, request);
call.enqueue(this);
}
/**
* Creates the next JSON message to send to remote JS executor, with request ID pre-filled in.
*/
private JsonGenerator startMessageObject(int requestID) throws IOException {
JsonGenerator jg = mJsonFactory.createGenerator(new StringWriter());
jg.writeStartObject();
jg.writeNumberField("id", requestID);
return jg;
}
/**
* Takes in a JsonGenerator created by {@link #startMessageObject} and returns the stringified
* JSON
*/
private String endMessageObject(JsonGenerator jg) throws IOException {
jg.writeEndObject();
jg.flush();
return ((StringWriter) jg.getOutputTarget()).getBuffer().toString();
}
public void prepareJSRuntime(JSDebuggerCallback callback) {
int requestID = mRequestID.getAndIncrement();
mCallbacks.put(requestID, callback);
try {
JsonGenerator jg = startMessageObject(requestID);
jg.writeStringField("method", "prepareJSRuntime");
sendMessage(requestID, endMessageObject(jg));
} catch (IOException e) {
triggerRequestFailure(requestID, e);
}
}
public void executeApplicationScript(
String sourceURL,
HashMap<String, String> injectedObjects,
JSDebuggerCallback callback) {
int requestID = mRequestID.getAndIncrement();
mCallbacks.put(requestID, callback);
try {
JsonGenerator jg = startMessageObject(requestID);
jg.writeStringField("method", "executeApplicationScript");
jg.writeStringField("url", sourceURL);
jg.writeObjectFieldStart("inject");
for (String key : injectedObjects.keySet()) {
jg.writeObjectField(key, injectedObjects.get(key));
}
jg.writeEndObject();
sendMessage(requestID, endMessageObject(jg));
} catch (IOException e) {
triggerRequestFailure(requestID, e);
}
}
public void executeJSCall(
String moduleName,
String methodName,
String jsonArgsArray,
JSDebuggerCallback callback) {
int requestID = mRequestID.getAndIncrement();
mCallbacks.put(requestID, callback);
try {
JsonGenerator jg = startMessageObject(requestID);
jg.writeStringField("method","executeJSCall");
jg.writeStringField("moduleName", moduleName);
jg.writeStringField("moduleMethod", methodName);
jg.writeFieldName("arguments");
jg.writeRawValue(jsonArgsArray);
sendMessage(requestID, endMessageObject(jg));
} catch (IOException e) {
triggerRequestFailure(requestID, e);
}
}
public void closeQuietly() {
if (mWebSocket != null) {
try {
mWebSocket.close(1000, "End of session");
} catch (IOException e) {
// swallow, no need to handle it here
}
mWebSocket = null;
}
}
private void sendMessage(int requestID, String message) {
if (mWebSocket == null) {
triggerRequestFailure(
requestID,
new IllegalStateException("WebSocket connection no longer valid"));
return;
}
Buffer messageBuffer = new Buffer();
messageBuffer.writeUtf8(message);
try {
mWebSocket.sendMessage(WebSocket.PayloadType.TEXT, messageBuffer);
} catch (IOException e) {
triggerRequestFailure(requestID, e);
}
}
private void triggerRequestFailure(int requestID, Throwable cause) {
JSDebuggerCallback callback = mCallbacks.get(requestID);
if (callback != null) {
mCallbacks.remove(requestID);
callback.onFailure(cause);
}
}
private void triggerRequestSuccess(int requestID, @Nullable String response) {
JSDebuggerCallback callback = mCallbacks.get(requestID);
if (callback != null) {
mCallbacks.remove(requestID);
callback.onSuccess(response);
}
}
@Override
public void onMessage(BufferedSource payload, WebSocket.PayloadType type) throws IOException {
if (type != WebSocket.PayloadType.TEXT) {
FLog.w(TAG, "Websocket received unexpected message with payload of type " + type);
return;
}
String message = null;
try {
message = payload.readUtf8();
} finally {
payload.close();
}
Integer replyID = null;
try {
JsonParser parser = new JsonFactory().createParser(message);
String result = null;
while (parser.nextToken() != JsonToken.END_OBJECT) {
String field = parser.getCurrentName();
if ("replyID".equals(field)) {
parser.nextToken();
replyID = parser.getIntValue();
} else if ("result".equals(field)) {
parser.nextToken();
result = parser.getText();
}
}
if (replyID != null) {
triggerRequestSuccess(replyID, result);
}
} catch (IOException e) {
if (replyID != null) {
triggerRequestFailure(replyID, e);
} else {
abort("Parsing response message from websocket failed", e);
}
}
}
@Override
public void onFailure(IOException e, Response response) {
abort("Websocket exception", e);
}
@Override
public void onOpen(WebSocket webSocket, Response response) {
mWebSocket = webSocket;
Assertions.assertNotNull(mConnectCallback).onSuccess(null);
mConnectCallback = null;
}
@Override
public void onClose(int code, String reason) {
mWebSocket = null;
}
@Override
public void onPong(Buffer payload) {
// ignore
}
private void abort(String message, Throwable cause) {
FLog.e(TAG, "Error occurred, shutting down websocket connection: " + message, cause);
closeQuietly();
// Trigger failure callbacks
if (mConnectCallback != null) {
mConnectCallback.onFailure(cause);
mConnectCallback = null;
}
for (JSDebuggerCallback callback : mCallbacks.values()) {
callback.onFailure(cause);
}
mCallbacks.clear();
}
}
@@ -0,0 +1,25 @@
/**
* 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 com.facebook.jni.Countable;
import com.facebook.proguard.annotations.DoNotStrip;
@DoNotStrip
public abstract class JavaScriptExecutor extends Countable {
/**
* Close this executor and cleanup any resources that it was using. No further calls are
* expected after this.
*/
public void close() {
}
}
@@ -0,0 +1,29 @@
/**
* 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 com.facebook.proguard.annotations.DoNotStrip;
/**
* Interface denoting that a class is the interface to a module with the same name in JS. Calling
* functions on this interface will result in corresponding methods in JS being called.
*
* When extending JavaScriptModule and registering it with a CatalystInstance, all public methods
* are assumed to be implemented on a JS module with the same name as this class. Calling methods
* on the object returned from {@link ReactContext#getJSModule} or
* {@link CatalystInstance#getJSModule} will result in the methods with those names exported by
* that module being called in JS.
*
* NB: JavaScriptModule does not allow method name overloading because JS does not allow method name
* overloading.
*/
@DoNotStrip
public interface JavaScriptModule {
}
@@ -0,0 +1,96 @@
/**
* 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.concurrent.Immutable;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Map;
import java.util.Set;
import com.facebook.react.common.MapBuilder;
import com.facebook.infer.annotation.Assertions;
/**
* Registration info for a {@link JavaScriptModule}. Maps its methods to method ids.
*/
@Immutable
class JavaScriptModuleRegistration {
private final int mModuleId;
private final Class<? extends JavaScriptModule> mModuleInterface;
private final Map<Method, Integer> mMethodsToIds;
private final Map<Method, String> mMethodsToTracingNames;
JavaScriptModuleRegistration(int moduleId, Class<? extends JavaScriptModule> moduleInterface) {
mModuleId = moduleId;
mModuleInterface = moduleInterface;
mMethodsToIds = MapBuilder.newHashMap();
mMethodsToTracingNames = MapBuilder.newHashMap();
final Method[] declaredMethods = mModuleInterface.getDeclaredMethods();
Arrays.sort(declaredMethods, new Comparator<Method>() {
@Override
public int compare(Method lhs, Method rhs) {
return lhs.getName().compareTo(rhs.getName());
}
});
// Methods are sorted by name so we can dupe check and have obvious ordering
String previousName = null;
for (int i = 0; i < declaredMethods.length; i++) {
Method method = declaredMethods[i];
String name = method.getName();
Assertions.assertCondition(
!name.equals(previousName),
"Method overloading is unsupported: " + mModuleInterface.getName() + "#" + name);
previousName = name;
mMethodsToIds.put(method, i);
mMethodsToTracingNames.put(method, "JSCall__" + getName() + "_" + method.getName());
}
}
public int getModuleId() {
return mModuleId;
}
public int getMethodId(Method method) {
final Integer id = mMethodsToIds.get(method);
Assertions.assertNotNull(id, "Unknown method: " + method.getName());
return id.intValue();
}
public String getTracingName(Method method) {
return Assertions.assertNotNull(mMethodsToTracingNames.get(method));
}
public Class<? extends JavaScriptModule> getModuleInterface() {
return mModuleInterface;
}
public String getName() {
// With proguard obfuscation turned on, proguard apparently (poorly) emulates inner classes or
// something because Class#getSimpleName() no longer strips the outer class name. We manually
// strip it here if necessary.
String name = mModuleInterface.getSimpleName();
int dollarSignIndex = name.lastIndexOf('$');
if (dollarSignIndex != -1) {
name = name.substring(dollarSignIndex + 1);
}
return name;
}
public Set<Method> getMethods() {
return mMethodsToIds.keySet();
}
}
@@ -0,0 +1,76 @@
/**
* 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.lang.Class;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import com.facebook.infer.annotation.Assertions;
/**
* Class responsible for holding all the {@link JavaScriptModule}s registered to this
* {@link CatalystInstance}. Uses Java proxy objects to dispatch method calls on JavaScriptModules
* to the bridge using the corresponding module and method ids so the proper function is executed in
* JavaScript.
*/
/*package*/ class JavaScriptModuleRegistry {
private final HashMap<Class<? extends JavaScriptModule>, JavaScriptModule> mModuleInstances;
public JavaScriptModuleRegistry(
CatalystInstance instance,
JavaScriptModulesConfig config) {
mModuleInstances = new HashMap<>();
for (JavaScriptModuleRegistration registration : config.getModuleDefinitions()) {
Class<? extends JavaScriptModule> moduleInterface = registration.getModuleInterface();
JavaScriptModule interfaceProxy = (JavaScriptModule) Proxy.newProxyInstance(
moduleInterface.getClassLoader(),
new Class[]{moduleInterface},
new JavaScriptModuleInvocationHandler(instance, registration));
mModuleInstances.put(moduleInterface, interfaceProxy);
}
}
public <T extends JavaScriptModule> T getJavaScriptModule(Class<T> moduleInterface) {
return (T) Assertions.assertNotNull(
mModuleInstances.get(moduleInterface),
"JS module " + moduleInterface.getSimpleName() + " hasn't been registered!");
}
private static class JavaScriptModuleInvocationHandler implements InvocationHandler {
private final CatalystInstance mCatalystInstance;
private final JavaScriptModuleRegistration mModuleRegistration;
public JavaScriptModuleInvocationHandler(
CatalystInstance catalystInstance,
JavaScriptModuleRegistration moduleRegistration) {
mCatalystInstance = catalystInstance;
mModuleRegistration = moduleRegistration;
}
@Override
public @Nullable Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String tracingName = mModuleRegistration.getTracingName(method);
mCatalystInstance.callFunction(
mModuleRegistration.getModuleId(),
mModuleRegistration.getMethodId(method),
Arguments.fromJavaArgs(args),
tracingName);
return null;
}
}
}
@@ -0,0 +1,84 @@
/**
* 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 java.io.IOException;
import java.io.StringWriter;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerator;
/**
* Class stores configuration of javascript modules that can be used across the bridge
*/
public class JavaScriptModulesConfig {
private final List<JavaScriptModuleRegistration> mModules;
private JavaScriptModulesConfig(List<JavaScriptModuleRegistration> modules) {
mModules = modules;
}
/*package*/ List<JavaScriptModuleRegistration> getModuleDefinitions() {
return mModules;
}
/*package*/ String moduleDescriptions() {
JsonFactory jsonFactory = new JsonFactory();
StringWriter writer = new StringWriter();
try {
JsonGenerator jg = jsonFactory.createGenerator(writer);
jg.writeStartObject();
for (JavaScriptModuleRegistration registration : mModules) {
jg.writeObjectFieldStart(registration.getName());
appendJSModuleToJSONObject(jg, registration);
jg.writeEndObject();
}
jg.writeEndObject();
jg.close();
} catch (IOException ioe) {
throw new RuntimeException("Unable to serialize JavaScript module declaration", ioe);
}
return writer.getBuffer().toString();
}
private void appendJSModuleToJSONObject(
JsonGenerator jg,
JavaScriptModuleRegistration registration) throws IOException {
jg.writeObjectField("moduleID", registration.getModuleId());
jg.writeObjectFieldStart("methods");
for (Method method : registration.getMethods()) {
jg.writeObjectFieldStart(method.getName());
jg.writeObjectField("methodID", registration.getMethodId(method));
jg.writeEndObject();
}
jg.writeEndObject();
}
public static class Builder {
private int mLastJSModuleId = 0;
private List<JavaScriptModuleRegistration> mModules =
new ArrayList<JavaScriptModuleRegistration>();
public Builder add(Class<? extends JavaScriptModule> moduleInterfaceClass) {
int moduleId = mLastJSModuleId++;
mModules.add(new JavaScriptModuleRegistration(moduleId, moduleInterfaceClass));
return this;
}
public JavaScriptModulesConfig build() {
return new JavaScriptModulesConfig(mModules);
}
}
}
@@ -0,0 +1,54 @@
/**
* 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 java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.fasterxml.jackson.core.JsonGenerator;
/**
* Helper for generating JSON for lists and maps.
*/
public class JsonGeneratorHelper {
/**
* Like {@link JsonGenerator#writeObjectField(String, Object)} but supports Maps and Lists.
*/
public static void writeObjectField(JsonGenerator jg, String name, Object object)
throws IOException {
if (object instanceof Map) {
writeMap(jg, name, (Map) object);
} else if (object instanceof List) {
writeList(jg, name, (List) object);
} else {
jg.writeObjectField(name, object);
}
}
private static void writeMap(JsonGenerator jg, String name, Map map) throws IOException {
jg.writeObjectFieldStart(name);
Set<Map.Entry> entries = map.entrySet();
for (Map.Entry entry : entries) {
writeObjectField(jg, entry.getKey().toString(), entry.getValue());
}
jg.writeEndObject();
}
private static void writeList(JsonGenerator jg, String name, List list) throws IOException {
jg.writeArrayFieldStart(name);
for (Object item : list) {
jg.writeObject(item);
}
jg.writeEndArray();
}
}
@@ -0,0 +1,32 @@
/**
* 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;
/**
* Listener for receiving activity/service lifecycle events.
*/
public interface LifecycleEventListener {
/**
* Called when host (activity/service) receives resume event (e.g. {@link Activity#onResume}
*/
void onHostResume();
/**
* Called when host (activity/service) receives pause event (e.g. {@link Activity#onPause}
*/
void onHostPause();
/**
* Called when host (activity/service) receives destroy event (e.g. {@link Activity#onDestroy}
*/
void onHostDestroy();
}
@@ -0,0 +1,26 @@
/**
* 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;
/**
* Exception thrown when a native module method call receives unexpected arguments from JS.
*/
public class NativeArgumentsParseException extends JSApplicationCausedNativeException {
public NativeArgumentsParseException(String detailMessage) {
super(detailMessage);
}
public NativeArgumentsParseException(@Nullable String detailMessage, @Nullable Throwable t) {
super(detailMessage, t);
}
}
@@ -0,0 +1,36 @@
/**
* 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 com.facebook.jni.HybridData;
import com.facebook.proguard.annotations.DoNotStrip;
import com.facebook.soloader.SoLoader;
/**
* Base class for an array whose members are stored in native code (C++).
*/
@DoNotStrip
public abstract class NativeArray {
static {
SoLoader.loadLibrary(ReactBridge.REACT_NATIVE_LIB);
}
public NativeArray() {
mHybridData = initHybrid();
}
@Override
public native String toString();
private native HybridData initHybrid();
@DoNotStrip
private HybridData mHybridData;
}
@@ -0,0 +1,34 @@
/**
* 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 com.facebook.jni.Countable;
import com.facebook.proguard.annotations.DoNotStrip;
import com.facebook.soloader.SoLoader;
/**
* Base class for a Map whose keys and values are stored in native code (C++).
*/
@DoNotStrip
public abstract class NativeMap extends Countable {
static {
SoLoader.loadLibrary(ReactBridge.REACT_NATIVE_LIB);
}
public NativeMap() {
initialize();
}
@Override
public native String toString();
private native void initialize();
}
@@ -0,0 +1,57 @@
/**
* 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 java.io.IOException;
import java.util.Map;
import com.fasterxml.jackson.core.JsonGenerator;
/**
* A native module whose API can be provided to JS catalyst instances. {@link NativeModule}s whose
* implementation is written in Java should extend {@link BaseJavaModule} or {@link
* ReactContextBaseJavaModule}. {@link NativeModule}s whose implementation is written in C++
* must not provide any Java code (so they can be reused on other platforms), and instead should
* register themselves using {@link CxxModuleWrapper}.
*/
public interface NativeModule {
public static interface NativeMethod {
void invoke(CatalystInstance catalystInstance, ReadableNativeArray parameters);
}
/**
* @return the name of this module. This will be the name used to {@code require()} this module
* from javascript.
*/
public String getName();
/**
* @return methods callable from JS on this module
*/
public Map<String, NativeMethod> getMethods();
/**
* Append a field which represents the constants this module exports
* to JS. If no constants are exported this should do nothing.
*/
public void writeConstantsField(JsonGenerator jg, String fieldName) throws IOException;
/**
* This is called at the end of {@link CatalystApplicationFragment#createCatalystInstance()}
* after the CatalystInstance has been created, in order to initialize NativeModules that require
* the CatalystInstance or JS modules.
*/
public void initialize();
/**
* Called before {CatalystInstance#onHostDestroy}
*/
public void onCatalystInstanceDestroy();
}
@@ -0,0 +1,28 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.bridge;
/**
* Interface for a class that knows how to handle an Exception thrown by a native module invoked
* from JS. Since these Exceptions are triggered by JS calls (and can be fixed in JS), a
* common way to handle one is to show a error dialog and allow the developer to change and reload
* JS.
*
* We should also note that we have a unique stance on what 'caused' means: even if there'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.
*/
public interface NativeModuleCallExceptionHandler {
/**
* Do something to display or log the exception.
*/
void handleException(Exception e);
}
@@ -0,0 +1,200 @@
/**
* 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 java.io.IOException;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import com.facebook.react.common.MapBuilder;
import com.facebook.react.common.SetBuilder;
import com.facebook.infer.annotation.Assertions;
import com.facebook.systrace.Systrace;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerator;
/**
* A set of Java APIs to expose to a particular JavaScript instance.
*/
public class NativeModuleRegistry {
private final ArrayList<ModuleDefinition> mModuleTable;
private final Map<Class<NativeModule>, NativeModule> mModuleInstances;
private final String mModuleDescriptions;
private final ArrayList<OnBatchCompleteListener> mBatchCompleteListenerModules;
private NativeModuleRegistry(
ArrayList<ModuleDefinition> moduleTable,
Map<Class<NativeModule>, NativeModule> moduleInstances,
String moduleDescriptions) {
mModuleTable = moduleTable;
mModuleInstances = moduleInstances;
mModuleDescriptions = moduleDescriptions;
mBatchCompleteListenerModules = new ArrayList<OnBatchCompleteListener>(mModuleTable.size());
for (int i = 0; i < mModuleTable.size(); i++) {
ModuleDefinition definition = mModuleTable.get(i);
if (definition.target instanceof OnBatchCompleteListener) {
mBatchCompleteListenerModules.add((OnBatchCompleteListener) definition.target);
}
}
}
/* package */ void call(
CatalystInstance catalystInstance,
int moduleId,
int methodId,
ReadableNativeArray parameters) {
ModuleDefinition definition = mModuleTable.get(moduleId);
if (definition == null) {
throw new RuntimeException("Call to unknown module: " + moduleId);
}
definition.call(catalystInstance, methodId, parameters);
}
/* package */ String moduleDescriptions() {
return mModuleDescriptions;
}
/* package */ void notifyCatalystInstanceDestroy() {
UiThreadUtil.assertOnUiThread();
for (NativeModule nativeModule : mModuleInstances.values()) {
nativeModule.onCatalystInstanceDestroy();
}
}
/* package */ void notifyCatalystInstanceInitialized() {
UiThreadUtil.assertOnUiThread();
for (NativeModule nativeModule : mModuleInstances.values()) {
nativeModule.initialize();
}
}
public void onBatchComplete() {
for (int i = 0; i < mBatchCompleteListenerModules.size(); i++) {
mBatchCompleteListenerModules.get(i).onBatchComplete();
}
}
public <T extends NativeModule> T getModule(Class<T> moduleInterface) {
return (T) Assertions.assertNotNull(mModuleInstances.get(moduleInterface));
}
public Collection<NativeModule> getAllModules() {
return mModuleInstances.values();
}
private static class ModuleDefinition {
public final int id;
public final String name;
public final NativeModule target;
public final ArrayList<MethodRegistration> methods;
public ModuleDefinition(int id, String name, NativeModule target) {
this.id = id;
this.name = name;
this.target = target;
this.methods = new ArrayList<MethodRegistration>();
for (Map.Entry<String, NativeModule.NativeMethod> entry : target.getMethods().entrySet()) {
this.methods.add(
new MethodRegistration(
entry.getKey(), "NativeCall__" + target.getName() + "_" + entry.getKey(),
entry.getValue()));
}
}
public void call(
CatalystInstance catalystInstance,
int methodId,
ReadableNativeArray parameters) {
MethodRegistration method = this.methods.get(methodId);
Systrace.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, method.tracingName);
try {
this.methods.get(methodId).method.invoke(catalystInstance, parameters);
} finally {
Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE);
}
}
}
private static class MethodRegistration {
public MethodRegistration(String name, String tracingName, NativeModule.NativeMethod method) {
this.name = name;
this.tracingName = tracingName;
this.method = method;
}
public String name;
public String tracingName;
public NativeModule.NativeMethod method;
}
public static class Builder {
private ArrayList<ModuleDefinition> mModuleDefinitions;
private Map<Class<NativeModule>, NativeModule> mModuleInstances;
private Set<String> mSeenModuleNames;
public Builder() {
mModuleDefinitions = new ArrayList<ModuleDefinition>();
mModuleInstances = MapBuilder.newHashMap();
mSeenModuleNames = SetBuilder.newHashSet();
}
public Builder add(NativeModule module) {
ModuleDefinition registration = new ModuleDefinition(
mModuleDefinitions.size(),
module.getName(),
module);
Assertions.assertCondition(
!mSeenModuleNames.contains(module.getName()),
"Module " + module.getName() + " was already registered!");
mSeenModuleNames.add(module.getName());
mModuleDefinitions.add(registration);
mModuleInstances.put((Class<NativeModule>) module.getClass(), module);
return this;
}
public NativeModuleRegistry build() {
JsonFactory jsonFactory = new JsonFactory();
StringWriter writer = new StringWriter();
try {
JsonGenerator jg = jsonFactory.createGenerator(writer);
jg.writeStartObject();
for (ModuleDefinition module : mModuleDefinitions) {
jg.writeObjectFieldStart(module.name);
jg.writeNumberField("moduleID", module.id);
jg.writeObjectFieldStart("methods");
for (int i = 0; i < module.methods.size(); i++) {
MethodRegistration method = module.methods.get(i);
jg.writeObjectFieldStart(method.name);
jg.writeNumberField("methodID", i);
jg.writeEndObject();
}
jg.writeEndObject();
module.target.writeConstantsField(jg, "constants");
jg.writeEndObject();
}
jg.writeEndObject();
jg.close();
} catch (IOException ioe) {
throw new RuntimeException("Unable to serialize Java module configuration", ioe);
}
String moduleDefinitionJson = writer.getBuffer().toString();
return new NativeModuleRegistry(mModuleDefinitions, mModuleInstances, moduleDefinitionJson);
}
}
}
@@ -0,0 +1,24 @@
/**
* 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 com.facebook.proguard.annotations.DoNotStrip;
/**
* Exception thrown by {@link ReadableNativeMap} when a key that does not exist is requested.
*/
@DoNotStrip
public class NoSuchKeyException extends RuntimeException {
@DoNotStrip
public NoSuchKeyException(String msg) {
super(msg);
}
}
@@ -0,0 +1,33 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.bridge;
/**
* Interface for receiving notification for bridge idle/busy events. Should not affect application
* logic and should only be used for debug/monitoring/testing purposes. Call
* {@link CatalystInstance#addBridgeIdleDebugListener} to start monitoring.
*
* NB: onTransitionToBridgeIdle and onTransitionToBridgeBusy may be called from different threads,
* and those threads may not be the same thread on which the listener was originally registered.
*/
public interface NotThreadSafeBridgeIdleDebugListener {
/**
* Called once all pending JS calls have resolved via an onBatchComplete call in the bridge and
* the requested native module calls have also run. The bridge will not become busy again until
* a timer, touch event, etc. causes a Java->JS call to be enqueued again.
*/
void onTransitionToBridgeIdle();
/**
* Called when the bridge was in an idle state and executes a JS call or callback.
*/
void onTransitionToBridgeBusy();
}
@@ -0,0 +1,26 @@
/**
* 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 com.facebook.proguard.annotations.DoNotStrip;
/**
* Exception thrown when a caller attempts to modify or use a {@link WritableNativeArray} or
* {@link WritableNativeMap} after it has already been added to a parent array or map. This is
* unsafe since we reuse the native memory so the underlying array/map is no longer valid.
*/
@DoNotStrip
public class ObjectAlreadyConsumedException extends RuntimeException {
@DoNotStrip
public ObjectAlreadyConsumedException(String detailMessage) {
super(detailMessage);
}
}
@@ -0,0 +1,18 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.bridge;
/**
* Interface for a module that will be notified when a batch of JS->Java calls has finished.
*/
public interface OnBatchCompleteListener {
void onBatchComplete();
}
@@ -0,0 +1,96 @@
/**
* 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 com.facebook.soloader.SoLoader;
import com.facebook.proguard.annotations.DoNotStrip;
/**
* JavaScript executor that delegates JS calls processed by native code back to a java version
* of the native executor interface.
*
* When set as a executor with {@link CatalystInstance.Builder}, catalyst native code will delegate
* low level javascript calls to the implementation of {@link JavaJSExecutor} interface provided
* with the constructor of this class.
*/
@DoNotStrip
public class ProxyJavaScriptExecutor extends JavaScriptExecutor {
static {
SoLoader.loadLibrary(ReactBridge.REACT_NATIVE_LIB);
}
public static class ProxyExecutorException extends Exception {
public ProxyExecutorException(Throwable cause) {
super(cause);
}
}
/**
* This is class represents java version of native js executor interface. When set through
* {@link ProxyJavaScriptExecutor} as a {@link CatalystInstance} executor, native code will
* delegate js calls to the given implementation of this interface.
*/
@DoNotStrip
public interface JavaJSExecutor {
/**
* Close this executor and cleanup any resources that it was using. No further calls are
* expected after this.
*/
void close();
/**
* Load javascript into the js context
* @param script script contet to be executed
* @param sourceURL url or file location from which script content was loaded
*/
@DoNotStrip
void executeApplicationScript(String script, String sourceURL) throws ProxyExecutorException;
/**
* Execute javascript method within js context
* @param modulename name of the common-js like module to execute the method from
* @param methodName name of the method to be executed
* @param jsonArgsArray json encoded array of arguments provided for the method call
* @return json encoded value returned from the method call
*/
@DoNotStrip
String executeJSCall(String modulename, String methodName, String jsonArgsArray)
throws ProxyExecutorException;
@DoNotStrip
void setGlobalVariable(String propertyName, String jsonEncodedValue);
}
private @Nullable JavaJSExecutor mJavaJSExecutor;
/**
* Create {@link ProxyJavaScriptExecutor} instance
* @param executor implementation of {@link JavaJSExecutor} which will be responsible for handling
* javascript calls
*/
public ProxyJavaScriptExecutor(JavaJSExecutor executor) {
mJavaJSExecutor = executor;
initialize(executor);
}
@Override
public void close() {
if (mJavaJSExecutor != null) {
mJavaJSExecutor.close();
mJavaJSExecutor = null;
}
}
private native void initialize(JavaJSExecutor executor);
}
@@ -0,0 +1,25 @@
/**
* 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 android.content.Context;
/**
* A context wrapper that always wraps Android Application {@link Context} and
* {@link CatalystInstance} by extending {@link ReactContext}
*/
public class ReactApplicationContext extends ReactContext {
// We want to wrap ApplicationContext, since there is no easy way to verify that application
// context is passed as a param, we use {@link Context#getApplicationContext} to ensure that
// the context we're wrapping is in fact an application context.
public ReactApplicationContext(Context context) {
super(context.getApplicationContext());
}
}
@@ -0,0 +1,71 @@
/**
* 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 android.content.res.AssetManager;
import com.facebook.react.bridge.queue.MessageQueueThread;
import com.facebook.jni.Countable;
import com.facebook.proguard.annotations.DoNotStrip;
import com.facebook.soloader.SoLoader;
/**
* Interface to the JS execution environment and means of transport for messages Java<->JS.
*/
@DoNotStrip
public class ReactBridge extends Countable {
/* package */ static final String REACT_NATIVE_LIB = "reactnativejni";
static {
SoLoader.loadLibrary(REACT_NATIVE_LIB);
}
private final ReactCallback mCallback;
private final JavaScriptExecutor mJSExecutor;
private final MessageQueueThread mNativeModulesQueueThread;
/**
* @param jsExecutor the JS executor to use to run JS
* @param callback the callback class used to invoke native modules
* @param nativeModulesQueueThread the MessageQueueThread the callbacks should be invoked on
*/
public ReactBridge(
JavaScriptExecutor jsExecutor,
ReactCallback callback,
MessageQueueThread nativeModulesQueueThread) {
mJSExecutor = jsExecutor;
mCallback = callback;
mNativeModulesQueueThread = nativeModulesQueueThread;
initialize(jsExecutor, callback, mNativeModulesQueueThread);
}
@Override
public void dispose() {
mJSExecutor.close();
mJSExecutor.dispose();
super.dispose();
}
private native void initialize(
JavaScriptExecutor jsExecutor,
ReactCallback callback,
MessageQueueThread nativeModulesQueueThread);
public native void loadScriptFromAssets(AssetManager assetManager, String assetName);
public native void loadScriptFromNetworkCached(String sourceURL, @Nullable String tempFileName);
public native void callFunction(int moduleId, int methodId, NativeArray arguments);
public native void invokeCallback(int callbackID, NativeArray arguments);
public native void setGlobalVariable(String propertyName, String jsonEncodedArgument);
public native boolean supportsProfiling();
public native void startProfiler(String title);
public native void stopProfiler(String title, String filename);
}
@@ -0,0 +1,22 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.bridge;
import com.facebook.proguard.annotations.DoNotStrip;
@DoNotStrip
public interface ReactCallback {
@DoNotStrip
void call(int moduleId, int methodId, ReadableNativeArray parameters);
@DoNotStrip
void onBatchComplete();
}
@@ -0,0 +1,202 @@
/**
* 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.util.concurrent.CopyOnWriteArraySet;
import android.content.Context;
import android.content.ContextWrapper;
import android.view.LayoutInflater;
import com.facebook.react.bridge.queue.CatalystQueueConfiguration;
import com.facebook.react.bridge.queue.MessageQueueThread;
import com.facebook.infer.annotation.Assertions;
/**
* Abstract ContextWrapper for Android applicaiton or activity {@link Context} and
* {@link CatalystInstance}
*/
public class ReactContext extends ContextWrapper {
private final CopyOnWriteArraySet<LifecycleEventListener> mLifecycleEventListeners =
new CopyOnWriteArraySet<>();
private @Nullable CatalystInstance mCatalystInstance;
private @Nullable LayoutInflater mInflater;
private @Nullable MessageQueueThread mUiMessageQueueThread;
private @Nullable MessageQueueThread mNativeModulesMessageQueueThread;
private @Nullable MessageQueueThread mJSMessageQueueThread;
private @Nullable NativeModuleCallExceptionHandler mNativeModuleCallExceptionHandler;
public ReactContext(Context base) {
super(base);
}
/**
* Set and initialize CatalystInstance for this Context. This should be called exactly once.
*/
public void initializeWithInstance(CatalystInstance catalystInstance) {
if (catalystInstance == null) {
throw new IllegalArgumentException("CatalystInstance cannot be null.");
}
if (mCatalystInstance != null) {
throw new IllegalStateException("ReactContext has been already initialized");
}
mCatalystInstance = catalystInstance;
CatalystQueueConfiguration queueConfig = catalystInstance.getCatalystQueueConfiguration();
mUiMessageQueueThread = queueConfig.getUIQueueThread();
mNativeModulesMessageQueueThread = queueConfig.getNativeModulesQueueThread();
mJSMessageQueueThread = queueConfig.getJSQueueThread();
}
public void setNativeModuleCallExceptionHandler(
@Nullable NativeModuleCallExceptionHandler nativeModuleCallExceptionHandler) {
mNativeModuleCallExceptionHandler = nativeModuleCallExceptionHandler;
}
// We override the following method so that views inflated with the inflater obtained from this
// context return the ReactContext in #getContext(). The default implementation uses the base
// context instead, so it couldn't be cast to ReactContext.
// TODO: T7538796 Check requirement for Override of getSystemService ReactContext
@Override
public Object getSystemService(String name) {
if (LAYOUT_INFLATER_SERVICE.equals(name)) {
if (mInflater == null) {
mInflater = LayoutInflater.from(getBaseContext()).cloneInContext(this);
}
return mInflater;
}
return getBaseContext().getSystemService(name);
}
/**
* @return handle to the specified JS module for the CatalystInstance associated with this Context
*/
public <T extends JavaScriptModule> T getJSModule(Class<T> jsInterface) {
if (mCatalystInstance == null) {
throw new RuntimeException("Trying to invoke JS before CatalystInstance has been set!");
}
return mCatalystInstance.getJSModule(jsInterface);
}
/**
* @return the instance of the specified module interface associated with this ReactContext.
*/
public <T extends NativeModule> T getNativeModule(Class<T> nativeModuleInterface) {
if (mCatalystInstance == null) {
throw new RuntimeException("Trying to invoke JS before CatalystInstance has been set!");
}
return mCatalystInstance.getNativeModule(nativeModuleInterface);
}
public CatalystInstance getCatalystInstance() {
return Assertions.assertNotNull(mCatalystInstance);
}
public boolean hasActiveCatalystInstance() {
return mCatalystInstance != null && !mCatalystInstance.isDestroyed();
}
public void addLifecycleEventListener(LifecycleEventListener listener) {
mLifecycleEventListeners.add(listener);
}
public void removeLifecycleEventListener(LifecycleEventListener listener) {
mLifecycleEventListeners.remove(listener);
}
/**
* Should be called by the hosting Fragment in {@link Fragment#onResume}
*/
public void onResume() {
UiThreadUtil.assertOnUiThread();
for (LifecycleEventListener listener : mLifecycleEventListeners) {
listener.onHostResume();
}
}
/**
* Should be called by the hosting Fragment in {@link Fragment#onPause}
*/
public void onPause() {
UiThreadUtil.assertOnUiThread();
for (LifecycleEventListener listener : mLifecycleEventListeners) {
listener.onHostPause();
}
}
/**
* Should be called by the hosting Fragment in {@link Fragment#onDestroy}
*/
public void onDestroy() {
UiThreadUtil.assertOnUiThread();
for (LifecycleEventListener listener : mLifecycleEventListeners) {
listener.onHostDestroy();
}
if (mCatalystInstance != null) {
mCatalystInstance.destroy();
}
}
public void assertOnUiQueueThread() {
Assertions.assertNotNull(mUiMessageQueueThread).assertIsOnThread();
}
public boolean isOnUiQueueThread() {
return Assertions.assertNotNull(mUiMessageQueueThread).isOnThread();
}
public void runOnUiQueueThread(Runnable runnable) {
Assertions.assertNotNull(mUiMessageQueueThread).runOnQueue(runnable);
}
public void assertOnNativeModulesQueueThread() {
Assertions.assertNotNull(mNativeModulesMessageQueueThread).assertIsOnThread();
}
public boolean isOnNativeModulesQueueThread() {
return Assertions.assertNotNull(mNativeModulesMessageQueueThread).isOnThread();
}
public void runOnNativeModulesQueueThread(Runnable runnable) {
Assertions.assertNotNull(mNativeModulesMessageQueueThread).runOnQueue(runnable);
}
public void assertOnJSQueueThread() {
Assertions.assertNotNull(mJSMessageQueueThread).assertIsOnThread();
}
public boolean isOnJSQueueThread() {
return Assertions.assertNotNull(mJSMessageQueueThread).isOnThread();
}
public void runOnJSQueueThread(Runnable runnable) {
Assertions.assertNotNull(mJSMessageQueueThread).runOnQueue(runnable);
}
/**
* Passes the given exception to the current
* {@link com.facebook.react.bridge.NativeModuleCallExceptionHandler} if one exists, rethrowing
* otherwise.
*/
public void handleException(RuntimeException e) {
if (mCatalystInstance != null &&
!mCatalystInstance.isDestroyed() &&
mNativeModuleCallExceptionHandler != null) {
mNativeModuleCallExceptionHandler.handleException(e);
} else {
throw e;
}
}
}
@@ -0,0 +1,30 @@
/**
* 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;
/**
* Base class for Catalyst native modules that require access to the {@link ReactContext}
* instance.
*/
public abstract class ReactContextBaseJavaModule extends BaseJavaModule {
private final ReactApplicationContext mReactApplicationContext;
public ReactContextBaseJavaModule(ReactApplicationContext reactContext) {
mReactApplicationContext = reactContext;
}
/**
* Subclasses can use this method to access catalyst context passed as a constructor
*/
protected final ReactApplicationContext getReactApplicationContext() {
return mReactApplicationContext;
}
}
@@ -0,0 +1,26 @@
/**
* 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 java.lang.annotation.Retention;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* Annotation which is used to mark methods that are exposed to
* Catalyst. This applies to derived classes of {@link
* BaseJavaModule}, which will generate a list of exported methods by
* searching for those which are annotated with this annotation and
* adding a JS callback for each.
*/
@Retention(RUNTIME)
public @interface ReactMethod {
}
@@ -0,0 +1,27 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.bridge;
/**
* Interface for an array that allows typed access to its members. Used to pass parameters from JS
* to Java.
*/
public interface ReadableArray {
int size();
boolean isNull(int index);
boolean getBoolean(int index);
double getDouble(int index);
int getInt(int index);
String getString(int index);
ReadableArray getArray(int index);
ReadableMap getMap(int index);
ReadableType getType(int index);
}
@@ -0,0 +1,28 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.bridge;
/**
* Interface for a map that allows typed access to its members. Used to pass parameters from JS to
* Java.
*/
public interface ReadableMap {
boolean hasKey(String name);
boolean isNull(String name);
boolean getBoolean(String name);
double getDouble(String name);
int getInt(String name);
String getString(String name);
ReadableArray getArray(String name);
ReadableMap getMap(String name);
ReadableType getType(String name);
ReadableMapKeySeyIterator keySetIterator();
}
@@ -0,0 +1,22 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.bridge;
import com.facebook.proguard.annotations.DoNotStrip;
/**
* Interface of a iterator for a {@link NativeMap}'s key set.
*/
@DoNotStrip
public interface ReadableMapKeySeyIterator {
boolean hasNextKey();
String nextKey();
}
@@ -0,0 +1,47 @@
/**
* 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 com.facebook.proguard.annotations.DoNotStrip;
import com.facebook.soloader.SoLoader;
/**
* Implementation of a NativeArray that allows read-only access to its members. This will generally
* be constructed and filled in native code so you shouldn't construct one yourself.
*/
@DoNotStrip
public class ReadableNativeArray extends NativeArray implements ReadableArray {
static {
SoLoader.loadLibrary(ReactBridge.REACT_NATIVE_LIB);
}
@Override
public native int size();
@Override
public native boolean isNull(int index);
@Override
public native boolean getBoolean(int index);
@Override
public native double getDouble(int index);
@Override
public native String getString(int index);
@Override
public native ReadableNativeArray getArray(int index);
@Override
public native ReadableNativeMap getMap(int index);
@Override
public native ReadableType getType(int index);
@Override
public int getInt(int index) {
return (int) getDouble(index);
}
}
@@ -0,0 +1,75 @@
/**
* 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 com.facebook.jni.Countable;
import com.facebook.proguard.annotations.DoNotStrip;
import com.facebook.soloader.SoLoader;
/**
* Implementation of a read-only map in native memory. This will generally be constructed and filled
* in native code so you shouldn't construct one yourself.
*/
@DoNotStrip
public class ReadableNativeMap extends NativeMap implements ReadableMap {
static {
SoLoader.loadLibrary(ReactBridge.REACT_NATIVE_LIB);
}
@Override
public native boolean hasKey(String name);
@Override
public native boolean isNull(String name);
@Override
public native boolean getBoolean(String name);
@Override
public native double getDouble(String name);
@Override
public native String getString(String name);
@Override
public native ReadableNativeArray getArray(String name);
@Override
public native ReadableNativeMap getMap(String name);
@Override
public native ReadableType getType(String name);
@Override
public ReadableMapKeySeyIterator keySetIterator() {
return new ReadableNativeMapKeySeyIterator(this);
}
@Override
public int getInt(String name) {
return (int) getDouble(name);
}
/**
* Implementation of a {@link ReadableNativeMap} iterator in native memory.
*/
@DoNotStrip
private static class ReadableNativeMapKeySeyIterator extends Countable
implements ReadableMapKeySeyIterator {
private final ReadableNativeMap mReadableNativeMap;
public ReadableNativeMapKeySeyIterator(ReadableNativeMap readableNativeMap) {
mReadableNativeMap = readableNativeMap;
initialize(mReadableNativeMap);
}
@Override
public native boolean hasNextKey();
@Override
public native String nextKey();
private native void initialize(ReadableNativeMap readableNativeMap);
}
}
@@ -0,0 +1,26 @@
/**
* 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 com.facebook.proguard.annotations.DoNotStrip;
/**
* Defines the type of an object stored in a {@link ReadableArray} or
* {@link ReadableMap}.
*/
@DoNotStrip
public enum ReadableType {
Null,
Boolean,
Number,
String,
Map,
Array,
}
@@ -0,0 +1,41 @@
/**
* 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;
/**
* Utility class to make assertions that should not hard-crash the app but instead be handled by the
* Catalyst app {@link NativeModuleCallExceptionHandler}. See the javadoc on that class for
* more information about our opinion on when these assertions should be used as opposed to
* assertions that might throw AssertionError Throwables that will cause the app to hard crash.
*/
public class SoftAssertions {
/**
* Asserts the given condition, throwing an {@link AssertionException} if the condition doesn't
* hold.
*/
public static void assertCondition(boolean condition, String message) {
if (!condition) {
throw new AssertionException(message);
}
}
/**
* Asserts that the given Object isn't null, throwing an {@link AssertionException} if it was.
*/
public static <T> T assertNotNull(@Nullable T instance) {
if (instance == null) {
throw new AssertionException("Expected object to not be null!");
}
return instance;
}
}
@@ -0,0 +1,56 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.bridge;
import javax.annotation.Nullable;
import android.os.Handler;
import android.os.Looper;
/**
* Utility for interacting with the UI thread.
*/
public class UiThreadUtil {
@Nullable private static Handler sMainHandler;
/**
* @return whether the current thread is the UI thread.
*/
public static boolean isOnUiThread() {
return Looper.getMainLooper().getThread() == Thread.currentThread();
}
/**
* Throws a {@link AssertionException} if the current thread is not the UI thread.
*/
public static void assertOnUiThread() {
SoftAssertions.assertCondition(isOnUiThread(), "Expected to run on UI thread!");
}
/**
* Throws a {@link AssertionException} if the current thread is the UI thread.
*/
public static void assertNotOnUiThread() {
SoftAssertions.assertCondition(!isOnUiThread(), "Expected not to run on UI thread!");
}
/**
* Runs the given Runnable on the UI thread.
*/
public static void runOnUiThread(Runnable runnable) {
synchronized (UiThreadUtil.class) {
if (sMainHandler == null) {
sMainHandler = new Handler(Looper.getMainLooper());
}
}
sMainHandler.post(runnable);
}
}
@@ -0,0 +1,25 @@
/**
* 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 com.facebook.proguard.annotations.DoNotStrip;
/**
* Exception thrown from native code when a type retrieved from a map or array (e.g. via
* {@link NativeArrayParameter#getString(int)}) does not match the expected type.
*/
@DoNotStrip
public class UnexpectedNativeTypeException extends RuntimeException {
@DoNotStrip
public UnexpectedNativeTypeException(String msg) {
super(msg);
}
}
@@ -0,0 +1,183 @@
/**
* 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.util.HashMap;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import android.os.Handler;
import com.facebook.infer.annotation.Assertions;
/**
* Executes JS remotely via the react nodejs server as a proxy to a browser on the host machine.
*/
public class WebsocketJavaScriptExecutor implements ProxyJavaScriptExecutor.JavaJSExecutor {
private static final long CONNECT_TIMEOUT_MS = 5000;
private static final int CONNECT_RETRY_COUNT = 3;
public interface JSExecutorConnectCallback {
void onSuccess();
void onFailure(Throwable cause);
}
public static class WebsocketExecutorTimeoutException extends Exception {
public WebsocketExecutorTimeoutException(String message) {
super(message);
}
}
private static class JSExecutorCallbackFuture implements
JSDebuggerWebSocketClient.JSDebuggerCallback {
private final Semaphore mSemaphore = new Semaphore(0);
private @Nullable Throwable mCause;
private @Nullable String mResponse;
@Override
public void onSuccess(@Nullable String response) {
mResponse = response;
mSemaphore.release();
}
@Override
public void onFailure(Throwable cause) {
mCause = cause;
mSemaphore.release();
}
/**
* Call only once per object instance!
*/
public @Nullable String get() throws Throwable {
mSemaphore.acquire();
if (mCause != null) {
throw mCause;
}
return mResponse;
}
}
final private HashMap<String, String> mInjectedObjects = new HashMap<>();
private @Nullable JSDebuggerWebSocketClient mWebSocketClient;
public void connect(final String webSocketServerUrl, final JSExecutorConnectCallback callback) {
final AtomicInteger retryCount = new AtomicInteger(CONNECT_RETRY_COUNT);
final JSExecutorConnectCallback retryProxyCallback = new JSExecutorConnectCallback() {
@Override
public void onSuccess() {
callback.onSuccess();
}
@Override
public void onFailure(Throwable cause) {
if (retryCount.decrementAndGet() <= 0) {
callback.onFailure(cause);
} else {
connectInternal(webSocketServerUrl, this);
}
}
};
connectInternal(webSocketServerUrl, retryProxyCallback);
}
private void connectInternal(
String webSocketServerUrl,
final JSExecutorConnectCallback callback) {
final JSDebuggerWebSocketClient client = new JSDebuggerWebSocketClient();
final Handler timeoutHandler = new Handler();
client.connect(
webSocketServerUrl, new JSDebuggerWebSocketClient.JSDebuggerCallback() {
@Override
public void onSuccess(@Nullable String response) {
client.prepareJSRuntime(
new JSDebuggerWebSocketClient.JSDebuggerCallback() {
@Override
public void onSuccess(@Nullable String response) {
timeoutHandler.removeCallbacksAndMessages(null);
mWebSocketClient = client;
callback.onSuccess();
}
@Override
public void onFailure(Throwable cause) {
timeoutHandler.removeCallbacksAndMessages(null);
callback.onFailure(cause);
}
});
}
@Override
public void onFailure(Throwable cause) {
callback.onFailure(cause);
}
});
timeoutHandler.postDelayed(
new Runnable() {
@Override
public void run() {
client.closeQuietly();
callback.onFailure(
new WebsocketExecutorTimeoutException(
"Timeout while connecting to remote debugger"));
}
},
CONNECT_TIMEOUT_MS);
}
@Override
public void close() {
if (mWebSocketClient != null) {
mWebSocketClient.closeQuietly();
}
}
@Override
public void executeApplicationScript(String script, String sourceURL)
throws ProxyJavaScriptExecutor.ProxyExecutorException {
JSExecutorCallbackFuture callback = new JSExecutorCallbackFuture();
Assertions.assertNotNull(mWebSocketClient).executeApplicationScript(
sourceURL,
mInjectedObjects,
callback);
try {
callback.get();
} catch (Throwable cause) {
throw new ProxyJavaScriptExecutor.ProxyExecutorException(cause);
}
}
@Override
public @Nullable String executeJSCall(String moduleName, String methodName, String jsonArgsArray)
throws ProxyJavaScriptExecutor.ProxyExecutorException {
JSExecutorCallbackFuture callback = new JSExecutorCallbackFuture();
Assertions.assertNotNull(mWebSocketClient).executeJSCall(
moduleName,
methodName,
jsonArgsArray,
callback);
try {
return callback.get();
} catch (Throwable cause) {
throw new ProxyJavaScriptExecutor.ProxyExecutorException(cause);
}
}
@Override
public void setGlobalVariable(String propertyName, String jsonEncodedValue) {
// Store and use in the next executeApplicationScript() call.
mInjectedObjects.put(propertyName, jsonEncodedValue);
}
}
@@ -0,0 +1,24 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.bridge;
/**
* Interface for a mutable array. Used to pass arguments from Java to JS.
*/
public interface WritableArray extends ReadableArray {
void pushNull();
void pushBoolean(boolean value);
void pushDouble(double value);
void pushInt(int value);
void pushString(String value);
void pushArray(WritableArray array);
void pushMap(WritableMap map);
}
@@ -0,0 +1,26 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.bridge;
/**
* Interface for a mutable map. Used to pass arguments from Java to JS.
*/
public interface WritableMap extends ReadableMap {
void putNull(String key);
void putBoolean(String key, boolean value);
void putDouble(String key, double value);
void putInt(String key, int value);
void putString(String key, String value);
void putArray(String key, WritableArray value);
void putMap(String key, WritableMap value);
void merge(ReadableMap source);
}
@@ -0,0 +1,60 @@
/**
* 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 com.facebook.infer.annotation.Assertions;
import com.facebook.proguard.annotations.DoNotStrip;
import com.facebook.soloader.SoLoader;
/**
* Implementation of a write-only array stored in native memory. Use
* {@link Arguments#createArray()} if you need to stub out creating this class in a test.
* TODO(5815532): Check if consumed on read
*/
@DoNotStrip
public class WritableNativeArray extends ReadableNativeArray implements WritableArray {
static {
SoLoader.loadLibrary(ReactBridge.REACT_NATIVE_LIB);
}
@Override
public native void pushNull();
@Override
public native void pushBoolean(boolean value);
@Override
public native void pushDouble(double value);
@Override
public native void pushString(String value);
@Override
public void pushInt(int value) {
pushDouble(value);
}
// Note: this consumes the map so do not reuse it.
@Override
public void pushArray(WritableArray array) {
Assertions.assertCondition(
array == null || array instanceof WritableNativeArray, "Illegal type provided");
pushNativeArray((WritableNativeArray) array);
}
// Note: this consumes the map so do not reuse it.
@Override
public void pushMap(WritableMap map) {
Assertions.assertCondition(
map == null || map instanceof WritableNativeMap, "Illegal type provided");
pushNativeMap((WritableNativeMap) map);
}
private native void pushNativeArray(WritableNativeArray array);
private native void pushNativeMap(WritableNativeMap map);
}
@@ -0,0 +1,68 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.bridge;
import com.facebook.infer.annotation.Assertions;
import com.facebook.proguard.annotations.DoNotStrip;
import com.facebook.soloader.SoLoader;
/**
* Implementation of a write-only map stored in native memory. Use
* {@link Arguments#createMap()} if you need to stub out creating this class in a test.
* TODO(5815532): Check if consumed on read
*/
@DoNotStrip
public class WritableNativeMap extends ReadableNativeMap implements WritableMap {
static {
SoLoader.loadLibrary(ReactBridge.REACT_NATIVE_LIB);
}
@Override
public native void putBoolean(String key, boolean value);
@Override
public native void putDouble(String key, double value);
@Override
public native void putString(String key, String value);
@Override
public native void putNull(String key);
@Override
public void putInt(String key, int value) {
putDouble(key, value);
}
// Note: this consumes the map so do not reuse it.
@Override
public void putMap(String key, WritableMap value) {
Assertions.assertCondition(
value == null || value instanceof WritableNativeMap, "Illegal type provided");
putNativeMap(key, (WritableNativeMap) value);
}
// Note: this consumes the map so do not reuse it.
@Override
public void putArray(String key, WritableArray value) {
Assertions.assertCondition(
value == null || value instanceof WritableNativeArray, "Illegal type provided");
putNativeArray(key, (WritableNativeArray) value);
}
// Note: this **DOES NOT** consume the source map
@Override
public void merge(ReadableMap source) {
Assertions.assertCondition(source instanceof ReadableNativeMap, "Illegal type provided");
mergeNativeMap((ReadableNativeMap) source);
}
private native void putNativeMap(String key, WritableNativeMap value);
private native void putNativeArray(String key, WritableNativeArray value);
private native void mergeNativeMap(ReadableNativeMap source);
}
@@ -0,0 +1,14 @@
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
import os
import sys
import zipfile
srcs = sys.argv[1:]
with zipfile.ZipFile(sys.stdout, 'w') as jar:
for src in srcs:
archive_name = os.path.join('assets/', os.path.basename(src))
jar.write(src, archive_name, zipfile.ZIP_DEFLATED)
@@ -0,0 +1,90 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.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
* the same but should be coded against as if they are different.
*
* UI Queue Thread: The standard Android main UI thread and Looper. Not configurable.
* 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<MessageQueueThreadSpec, MessageQueueThread> 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);
}
}
@@ -0,0 +1,79 @@
/**
* 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 javax.annotation.Nullable;
import com.facebook.infer.annotation.Assertions;
/**
* Spec for creating a CatalystQueueConfiguration. This exists so that CatalystInstance is able to
* set Exception handlers on the MessageQueueThreads it uses and it would not be super clean if the
* threads were configured, then passed to CatalystInstance where they are configured more. These
* specs allows the Threads to be created fully configured.
*/
public class CatalystQueueConfigurationSpec {
private final MessageQueueThreadSpec mNativeModulesQueueThreadSpec;
private final MessageQueueThreadSpec mJSQueueThreadSpec;
private CatalystQueueConfigurationSpec(
MessageQueueThreadSpec nativeModulesQueueThreadSpec,
MessageQueueThreadSpec jsQueueThreadSpec) {
mNativeModulesQueueThreadSpec = nativeModulesQueueThreadSpec;
mJSQueueThreadSpec = jsQueueThreadSpec;
}
public MessageQueueThreadSpec getNativeModulesQueueThreadSpec() {
return mNativeModulesQueueThreadSpec;
}
public MessageQueueThreadSpec getJSQueueThreadSpec() {
return mJSQueueThreadSpec;
}
public static Builder builder() {
return new Builder();
}
public static CatalystQueueConfigurationSpec createDefault() {
return builder()
.setJSQueueThreadSpec(MessageQueueThreadSpec.newBackgroundThreadSpec("js"))
.setNativeModulesQueueThreadSpec(
MessageQueueThreadSpec.newBackgroundThreadSpec("native_modules"))
.build();
}
public static class Builder {
private @Nullable MessageQueueThreadSpec mNativeModulesQueueSpec;
private @Nullable MessageQueueThreadSpec mJSQueueSpec;
public Builder setNativeModulesQueueThreadSpec(MessageQueueThreadSpec spec) {
Assertions.assertCondition(
mNativeModulesQueueSpec == null,
"Setting native modules queue spec multiple times!");
mNativeModulesQueueSpec = spec;
return this;
}
public Builder setJSQueueThreadSpec(MessageQueueThreadSpec spec) {
Assertions.assertCondition(mJSQueueSpec == null, "Setting JS queue multiple times!");
mJSQueueSpec = spec;
return this;
}
public CatalystQueueConfigurationSpec build() {
return new CatalystQueueConfigurationSpec(
Assertions.assertNotNull(mNativeModulesQueueSpec),
Assertions.assertNotNull(mJSQueueSpec));
}
}
}
@@ -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
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!";
}
/**
* 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 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<Looper> 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);
}
}
@@ -0,0 +1,36 @@
/**
* 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.Handler;
import android.os.Looper;
import android.os.Message;
/**
* Handler that can catch and dispatch Exceptions to an Exception handler.
*/
public class MessageQueueThreadHandler extends Handler {
private final QueueThreadExceptionHandler mExceptionHandler;
public MessageQueueThreadHandler(Looper looper, QueueThreadExceptionHandler exceptionHandler) {
super(looper);
mExceptionHandler = exceptionHandler;
}
@Override
public void dispatchMessage(Message msg) {
try {
super.dispatchMessage(msg);
} catch (Exception e) {
mExceptionHandler.handleException(e);
}
}
}
@@ -0,0 +1,48 @@
/**
* 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;
/**
* Spec for creating a MessageQueueThread.
*/
public class MessageQueueThreadSpec {
private static final MessageQueueThreadSpec MAIN_UI_SPEC =
new MessageQueueThreadSpec(ThreadType.MAIN_UI, "main_ui");
protected static enum ThreadType {
MAIN_UI,
NEW_BACKGROUND,
}
public static MessageQueueThreadSpec newBackgroundThreadSpec(String name) {
return new MessageQueueThreadSpec(ThreadType.NEW_BACKGROUND, name);
}
public static MessageQueueThreadSpec mainThreadSpec() {
return MAIN_UI_SPEC;
}
private final ThreadType mThreadType;
private final String mName;
private MessageQueueThreadSpec(ThreadType threadType, String name) {
mThreadType = threadType;
mName = name;
}
public ThreadType getThreadType() {
return mThreadType;
}
public String getName() {
return mName;
}
}
@@ -0,0 +1,29 @@
/**
* 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 com.facebook.jni.Countable;
import com.facebook.proguard.annotations.DoNotStrip;
/**
* A Runnable that has a native run implementation.
*/
@DoNotStrip
public class NativeRunnable extends Countable implements Runnable {
/**
* Should only be instantiated via native (JNI) code.
*/
@DoNotStrip
private NativeRunnable() {
}
public native void run();
}
@@ -0,0 +1,19 @@
/**
* 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;
/**
* Interface for a class that knows how to handle an Exception thrown while executing a Runnable
* submitted via {@link MessageQueueThread#runOnQueue}.
*/
public interface QueueThreadExceptionHandler {
void handleException(Exception e);
}
@@ -0,0 +1,78 @@
/**
* 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.common;
/**
* Object wrapping an auto-expanding long[]. Like an ArrayList<Long> but without the autoboxing.
*/
public class LongArray {
private static final double INNER_ARRAY_GROWTH_FACTOR = 1.8;
private long[] mArray;
private int mLength;
public static LongArray createWithInitialCapacity(int initialCapacity) {
return new LongArray(initialCapacity);
}
private LongArray(int initialCapacity) {
mArray = new long[initialCapacity];
mLength = 0;
}
public void add(long value) {
growArrayIfNeeded();
mArray[mLength++] = value;
}
public long get(int index) {
if (index >= mLength) {
throw new IndexOutOfBoundsException("" + index + " >= " + mLength);
}
return mArray[index];
}
public void set(int index, long value) {
if (index >= mLength) {
throw new IndexOutOfBoundsException("" + index + " >= " + mLength);
}
mArray[index] = value;
}
public int size() {
return mLength;
}
public boolean isEmpty() {
return mLength == 0;
}
/**
* Removes the *last* n items of the array all at once.
*/
public void dropTail(int n) {
if (n > mLength) {
throw new IndexOutOfBoundsException(
"Trying to drop " + n + " items from array of length " + mLength);
}
mLength -= n;
}
private void growArrayIfNeeded() {
if (mLength == mArray.length) {
// If the initial capacity was 1 we need to ensure it at least grows by 1.
int newSize = Math.max(mLength + 1, (int)(mLength * INNER_ARRAY_GROWTH_FACTOR));
long[] newArray = new long[newSize];
System.arraycopy(mArray, 0, newArray, 0, mLength);
mArray = newArray;
}
}
}
@@ -0,0 +1,154 @@
/**
* 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.common;
import java.util.HashMap;
import java.util.Map;
/**
* Utility class for creating maps
*/
public class MapBuilder {
/**
* Creates an instance of {@code HashMap}
*/
public static <K, V> HashMap<K, V> newHashMap() {
return new HashMap<K, V>();
}
/**
* Returns the empty map.
*/
public static <K, V> Map<K, V> of() {
return newHashMap();
}
/**
* Returns map containing a single entry.
*/
public static <K, V> Map<K, V> of(K k1, V v1) {
Map map = of();
map.put(k1, v1);
return map;
}
/**
* Returns map containing the given entries.
*/
public static <K, V> Map<K, V> of(K k1, V v1, K k2, V v2) {
Map map = of();
map.put(k1, v1);
map.put(k2, v2);
return map;
}
/**
* Returns map containing the given entries.
*/
public static <K, V> Map<K, V> of(K k1, V v1, K k2, V v2, K k3, V v3) {
Map map = of();
map.put(k1, v1);
map.put(k2, v2);
map.put(k3, v3);
return map;
}
/**
* Returns map containing the given entries.
*/
public static <K, V> Map<K, V> of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4) {
Map map = of();
map.put(k1, v1);
map.put(k2, v2);
map.put(k3, v3);
map.put(k4, v4);
return map;
}
/**
* Returns map containing the given entries.
*/
public static <K, V> Map<K, V> of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4, K k5, V v5) {
Map map = of();
map.put(k1, v1);
map.put(k2, v2);
map.put(k3, v3);
map.put(k4, v4);
map.put(k5, v5);
return map;
}
/**
* Returns map containing the given entries.
*/
public static <K, V> Map<K, V> of(
K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4, K k5, V v5, K k6, V v6) {
Map map = of();
map.put(k1, v1);
map.put(k2, v2);
map.put(k3, v3);
map.put(k4, v4);
map.put(k5, v5);
map.put(k6, v6);
return map;
}
/**
* Returns map containing the given entries.
*/
public static <K, V> Map<K, V> of(
K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4, K k5, V v5, K k6, V v6, K k7, V v7) {
Map map = of();
map.put(k1, v1);
map.put(k2, v2);
map.put(k3, v3);
map.put(k4, v4);
map.put(k5, v5);
map.put(k6, v6);
map.put(k7, v7);
return map;
}
/**
* Returns map containing the given entries.
*/
public static <K, V> Builder<K, V> builder() {
return new Builder();
}
public static final class Builder<K, V> {
private Map mMap;
private boolean mUnderConstruction;
private Builder() {
mMap = newHashMap();
mUnderConstruction = true;
}
public Builder<K,V> put(K k, V v) {
if (!mUnderConstruction) {
throw new IllegalStateException("Underlying map has already been built");
}
mMap.put(k,v);
return this;
}
public Map<K,V> build() {
if (!mUnderConstruction) {
throw new IllegalStateException("Underlying map has already been built");
}
mUnderConstruction = false;
return mMap;
}
}
}
@@ -0,0 +1,15 @@
/**
* 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.common;
public class ReactConstants {
public static final String TAG = "React";
}
@@ -0,0 +1,26 @@
/**
* 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.common;
import java.util.HashSet;
/**
* Utility class for creating sets
*/
public class SetBuilder {
/**
* Creates an instance of {@code HashSet}
*/
public static <E> HashSet<E> newHashSet() {
return new HashSet<E>();
}
}
@@ -0,0 +1,121 @@
/**
* 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.common;
import javax.annotation.Nullable;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import com.facebook.infer.annotation.Assertions;
/**
* Listens for the user shaking their phone. Allocation-less once it starts listening.
*/
public class ShakeDetector implements SensorEventListener {
private static final int MAX_SAMPLES = 25;
private static final int MIN_TIME_BETWEEN_SAMPLES_MS = 20;
private static final int VISIBLE_TIME_RANGE_MS = 500;
private static final int MAGNITUDE_THRESHOLD = 25;
private static final int PERCENT_OVER_THRESHOLD_FOR_SHAKE = 66;
public static interface ShakeListener {
void onShake();
}
private final ShakeListener mShakeListener;
@Nullable private SensorManager mSensorManager;
private long mLastTimestamp;
private int mCurrentIndex;
@Nullable private double[] mMagnitudes;
@Nullable private long[] mTimestamps;
public ShakeDetector(ShakeListener listener) {
mShakeListener = listener;
}
/**
* Start listening for shakes.
*/
public void start(SensorManager manager) {
Assertions.assertNotNull(manager);
Sensor accelerometer = manager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
if (accelerometer != null) {
mSensorManager = manager;
mLastTimestamp = -1;
mCurrentIndex = 0;
mMagnitudes = new double[MAX_SAMPLES];
mTimestamps = new long[MAX_SAMPLES];
mSensorManager.registerListener(this, accelerometer, SensorManager.SENSOR_DELAY_UI);
}
}
/**
* Stop listening for shakes.
*/
public void stop() {
if (mSensorManager != null) {
mSensorManager.unregisterListener(this);
mSensorManager = null;
}
}
@Override
public void onSensorChanged(SensorEvent sensorEvent) {
if (sensorEvent.timestamp - mLastTimestamp < MIN_TIME_BETWEEN_SAMPLES_MS) {
return;
}
Assertions.assertNotNull(mTimestamps);
Assertions.assertNotNull(mMagnitudes);
float ax = sensorEvent.values[0];
float ay = sensorEvent.values[1];
float az = sensorEvent.values[2];
mLastTimestamp = sensorEvent.timestamp;
mTimestamps[mCurrentIndex] = sensorEvent.timestamp;
mMagnitudes[mCurrentIndex] = Math.sqrt(ax * ax + ay * ay + az * az);
maybeDispatchShake(sensorEvent.timestamp);
mCurrentIndex = (mCurrentIndex + 1) % MAX_SAMPLES;
}
@Override
public void onAccuracyChanged(Sensor sensor, int i) {
}
private void maybeDispatchShake(long currentTimestamp) {
Assertions.assertNotNull(mTimestamps);
Assertions.assertNotNull(mMagnitudes);
int numOverThreshold = 0;
int total = 0;
for (int i = 0; i < MAX_SAMPLES; i++) {
int index = (mCurrentIndex - i + MAX_SAMPLES) % MAX_SAMPLES;
if (currentTimestamp - mTimestamps[index] < VISIBLE_TIME_RANGE_MS) {
total++;
if (mMagnitudes[index] >= MAGNITUDE_THRESHOLD) {
numOverThreshold++;
}
}
}
if (((double) numOverThreshold) / total > PERCENT_OVER_THRESHOLD_FOR_SHAKE / 100.0) {
mShakeListener.onShake();
}
}
}
@@ -0,0 +1,25 @@
/**
* 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.common;
/**
* Detour for System.currentTimeMillis and System.nanoTime calls so that they can be mocked out in
* tests.
*/
public class SystemClock {
public static long currentTimeMillis() {
return System.currentTimeMillis();
}
public static long nanoTime() {
return System.nanoTime();
}
}
@@ -0,0 +1,17 @@
/**
* 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.common.annotations;
/**
* Annotates a method that should have restricted visibility but it's required to be public for use
* in test code only.
*/
public @interface VisibleForTesting {
}
@@ -0,0 +1,62 @@
/**
* 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.common.futures;
import javax.annotation.Nullable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
/**
* A super simple Future-like class that can safely notify another Thread when a value is ready.
* Does not support setting errors or canceling.
*/
public class SimpleSettableFuture<T> {
private final CountDownLatch mReadyLatch = new CountDownLatch(1);
private volatile @Nullable T mResult;
/**
* Sets the result. If another thread has called {@link #get}, they will immediately receive the
* value. Must only be called once.
*/
public void set(T result) {
if (mReadyLatch.getCount() == 0) {
throw new RuntimeException("Result has already been set!");
}
mResult = result;
mReadyLatch.countDown();
}
/**
* Wait up to the timeout time for another Thread to set a value on this future. If a value has
* already been set, this method will return immediately.
*
* NB: For simplicity, we catch and wrap InterruptedException. Do NOT use this class if you
* are in the 1% of cases where you actually want to handle that.
*/
public @Nullable T get(long timeoutMS) {
try {
if (!mReadyLatch.await(timeoutMS, TimeUnit.MILLISECONDS)) {
throw new TimeoutException();
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return mResult;
}
public static class TimeoutException extends RuntimeException {
public TimeoutException() {
super("Timed out waiting for future");
}
}
}
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.facebook.react.devsupport">
<application>
<!-- This activity will be removed for release builds -->
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity"
android:exported="false"
/>
</application>
</manifest>
@@ -0,0 +1,54 @@
/**
* 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.devsupport;
import javax.annotation.Nullable;
import android.content.Context;
import android.graphics.PixelFormat;
import android.view.WindowManager;
import android.widget.FrameLayout;
import com.facebook.react.bridge.ReactContext;
/**
* Helper class for controlling overlay view with FPS and JS FPS info
* that gets added directly to @{link WindowManager} instance.
*/
/* package */ class DebugOverlayController {
private final WindowManager mWindowManager;
private final ReactContext mReactContext;
private @Nullable FrameLayout mFPSDebugViewContainer;
public DebugOverlayController(ReactContext reactContext) {
mReactContext = reactContext;
mWindowManager = (WindowManager) reactContext.getSystemService(Context.WINDOW_SERVICE);
}
public void setFpsDebugViewVisible(boolean fpsDebugViewVisible) {
if (fpsDebugViewVisible && mFPSDebugViewContainer == null) {
mFPSDebugViewContainer = new FpsView(mReactContext);
WindowManager.LayoutParams params = new WindowManager.LayoutParams(
WindowManager.LayoutParams.MATCH_PARENT,
WindowManager.LayoutParams.MATCH_PARENT,
WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE,
PixelFormat.TRANSLUCENT);
mWindowManager.addView(mFPSDebugViewContainer, params);
} else if (!fpsDebugViewVisible && mFPSDebugViewContainer != null) {
mFPSDebugViewContainer.removeAllViews();
mWindowManager.removeView(mFPSDebugViewContainer);
mFPSDebugViewContainer = null;
}
}
}
@@ -0,0 +1,74 @@
/**
* 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.devsupport;
import javax.annotation.Nullable;
import java.io.IOException;
import android.text.TextUtils;
import com.facebook.common.logging.FLog;
import com.facebook.react.common.ReactConstants;
import org.json.JSONException;
import org.json.JSONObject;
/**
* The debug server returns errors as json objects. This exception represents that error.
*/
public class DebugServerException extends IOException {
public final String description;
public final String fileName;
public final int lineNumber;
public final int column;
private DebugServerException(String description, String fileName, int lineNumber, int column) {
this.description = description;
this.fileName = fileName;
this.lineNumber = lineNumber;
this.column = column;
}
public String toReadableMessage() {
return description + "\n at " + fileName + ":" + lineNumber + ":" + column;
}
/**
* Parse a DebugServerException from the server json string.
* @param str json string returned by the debug server
* @return A DebugServerException or null if the string is not of proper form.
*/
@Nullable public static DebugServerException parse(String str) {
if (TextUtils.isEmpty(str)) {
return null;
}
try {
JSONObject jsonObject = new JSONObject(str);
String fullFileName = jsonObject.getString("filename");
return new DebugServerException(
jsonObject.getString("description"),
shortenFileName(fullFileName),
jsonObject.getInt("lineNumber"),
jsonObject.getInt("column"));
} catch (JSONException e) {
// I'm not sure how strict this format is for returned errors, or what other errors there can
// be, so this may end up being spammy. Can remove it later if necessary.
FLog.w(ReactConstants.TAG, "Could not parse DebugServerException from: " + str, e);
return null;
}
}
private static String shortenFileName(String fullFileName) {
String[] parts = fullFileName.split("/");
return parts[parts.length - 1];
}
}
@@ -0,0 +1,70 @@
/**
* 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.devsupport;
import javax.annotation.Nullable;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import com.facebook.react.common.annotations.VisibleForTesting;
import com.facebook.react.modules.debug.DeveloperSettings;
/**
* Helper class for accessing developers settings that should not be accessed outside of the package
* {@link com.facebook.react.devsupport}. For accessing some of the settings by external modules
* this class implements an external interface {@link DeveloperSettings}.
*/
@VisibleForTesting
public class DevInternalSettings implements
DeveloperSettings,
SharedPreferences.OnSharedPreferenceChangeListener {
private static final String PREFS_FPS_DEBUG_KEY = "fps_debug";
private static final String PREFS_DEBUG_SERVER_HOST_KEY = "debug_http_host";
private static final String PREFS_ANIMATIONS_DEBUG_KEY = "animations_debug";
private static final String PREFS_RELOAD_ON_JS_CHANGE_KEY = "reload_on_js_change";
private final SharedPreferences mPreferences;
private final DevSupportManager mDebugManager;
public DevInternalSettings(
Context applicationContext,
DevSupportManager debugManager) {
mDebugManager = debugManager;
mPreferences = PreferenceManager.getDefaultSharedPreferences(applicationContext);
mPreferences.registerOnSharedPreferenceChangeListener(this);
}
@Override
public boolean isFpsDebugEnabled() {
return mPreferences.getBoolean(PREFS_FPS_DEBUG_KEY, false);
}
@Override
public boolean isAnimationFpsDebugEnabled() {
return mPreferences.getBoolean(PREFS_ANIMATIONS_DEBUG_KEY, false);
}
public @Nullable String getDebugServerHost() {
return mPreferences.getString(PREFS_DEBUG_SERVER_HOST_KEY, null);
}
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (PREFS_FPS_DEBUG_KEY.equals(key) || PREFS_RELOAD_ON_JS_CHANGE_KEY.equals(key)) {
mDebugManager.reloadSettings();
}
}
public boolean isReloadOnJSChangeEnabled() {
return mPreferences.getBoolean(PREFS_RELOAD_ON_JS_CHANGE_KEY, false);
}
}
@@ -0,0 +1,25 @@
/**
* 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.devsupport;
/**
* Callback class for custom options that may appear in {@link DevSupportManager} developer
* options menu. In case when option registered for this handler is selected from the menu, the
* instance method {@link #onOptionSelected} will be triggered.
*/
public interface DevOptionHandler {
/**
* Triggered in case when user select custom developer option from the developers options menu
* displayed with {@link DevSupportManager}.
*/
public void onOptionSelected();
}
@@ -0,0 +1,307 @@
/**
* 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.devsupport;
import javax.annotation.Nullable;
import java.io.File;
import java.io.IOException;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
import android.content.Context;
import android.os.Build;
import android.os.Handler;
import android.text.TextUtils;
import com.facebook.common.logging.FLog;
import com.facebook.infer.annotation.Assertions;
import com.facebook.react.bridge.UiThreadUtil;
import com.facebook.react.common.ReactConstants;
import com.squareup.okhttp.Call;
import com.squareup.okhttp.Callback;
import com.squareup.okhttp.ConnectionPool;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.Response;
import okio.Okio;
import okio.Sink;
/**
* Helper class for all things about the debug server running in the engineer's host machine.
*
* One can use 'debug_http_host' shared preferences key to provide a host name for the debug server.
* If the setting is empty we support and detect two basic configuration that works well for android
* emulators connectiong to debug server running on emulator's host:
* - Android stock emulator with standard non-configurable local loopback alias: 10.0.2.2,
* - Genymotion emulator with default settings: 10.0.3.2
*/
/* package */ class DevServerHelper {
public static final String RELOAD_APP_EXTRA_JS_PROXY = "jsproxy";
private static final String RELOAD_APP_ACTION_SUFFIX = ".RELOAD_APP_ACTION";
private static final String EMULATOR_LOCALHOST = "10.0.2.2";
private static final String GENYMOTION_LOCALHOST = "10.0.3.2";
private static final String DEVICE_LOCALHOST = "localhost";
private static final String BUNDLE_URL_FORMAT =
"http://%s:8081/%s.bundle?platform=android";
private static final String SOURCE_MAP_URL_FORMAT =
BUNDLE_URL_FORMAT.replaceFirst("\\.bundle", ".map");
private static final String LAUNCH_CHROME_DEVTOOLS_COMMAND_URL_FORMAT =
"http://%s:8081/launch-chrome-devtools";
private static final String ONCHANGE_ENDPOINT_URL_FORMAT =
"http://%s:8081/onchange";
private static final String WEBSOCKET_PROXY_URL_FORMAT = "ws://%s:8081/debugger-proxy";
private static final int LONG_POLL_KEEP_ALIVE_DURATION_MS = 2 * 60 * 1000; // 2 mins
private static final int LONG_POLL_FAILURE_DELAY_MS = 5000;
private static final int HTTP_CONNECT_TIMEOUT_MS = 5000;
public interface BundleDownloadCallback {
void onSuccess();
void onFailure(Exception cause);
}
public interface OnServerContentChangeListener {
void onServerContentChanged();
}
private final DevInternalSettings mSettings;
private final OkHttpClient mClient;
private boolean mOnChangePollingEnabled;
private @Nullable OkHttpClient mOnChangePollingClient;
private @Nullable Handler mRestartOnChangePollingHandler;
private @Nullable OnServerContentChangeListener mOnServerContentChangeListener;
public DevServerHelper(DevInternalSettings settings) {
mSettings = settings;
mClient = new OkHttpClient();
mClient.setConnectTimeout(HTTP_CONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS);
// No read or write timeouts by default
mClient.setReadTimeout(0, TimeUnit.MILLISECONDS);
mClient.setWriteTimeout(0, TimeUnit.MILLISECONDS);
}
/** Intent action for reloading the JS */
public static String getReloadAppAction(Context context) {
return context.getPackageName() + RELOAD_APP_ACTION_SUFFIX;
}
public String getWebsocketProxyURL() {
return String.format(Locale.US, WEBSOCKET_PROXY_URL_FORMAT, getDebugServerHost());
}
/**
* @return the host to use when connecting to the bundle server from the host itself.
*/
private static String getHostForJSProxy() {
return "localhost";
}
/**
* @return the host to use when connecting to the bundle server.
*/
private String getDebugServerHost() {
// Check debug server host setting first. If empty try to detect emulator type and use default
// hostname for those
String hostFromSettings = mSettings.getDebugServerHost();
if (!TextUtils.isEmpty(hostFromSettings)) {
return Assertions.assertNotNull(hostFromSettings);
}
// Since genymotion runs in vbox it use different hostname to refer to adb host.
// We detect whether app runs on genymotion and replace js bundle server hostname accordingly
if (isRunningOnGenymotion()) {
return GENYMOTION_LOCALHOST;
}
if (isRunningOnStockEmulator()) {
return EMULATOR_LOCALHOST;
}
FLog.w(
ReactConstants.TAG,
"You seem to be running on device. Run 'adb reverse tcp:8081 tcp:8081' " +
"to forward the debug server's port to the device.");
return DEVICE_LOCALHOST;
}
private boolean isRunningOnGenymotion() {
return Build.FINGERPRINT.contains("vbox");
}
private boolean isRunningOnStockEmulator() {
return Build.FINGERPRINT.contains("generic");
}
private String createBundleURL(String host, String jsModulePath) {
return String.format(BUNDLE_URL_FORMAT, host, jsModulePath);
}
public void downloadBundleFromURL(
final BundleDownloadCallback callback,
final String jsModulePath,
final File outputFile) {
final String bundleURL = createBundleURL(getDebugServerHost(), jsModulePath);
Request request = new Request.Builder()
.url(bundleURL)
.build();
Call call = mClient.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(Request request, IOException e) {
callback.onFailure(e);
}
@Override
public void onResponse(Response response) throws IOException {
// Check for server errors. If the server error has the expected form, fail with more info.
if (!response.isSuccessful()) {
String body = response.body().string();
DebugServerException debugServerException = DebugServerException.parse(body);
if (debugServerException != null) {
callback.onFailure(debugServerException);
} else {
callback.onFailure(new IOException("Unexpected response code: " + response.code()));
}
return;
}
Sink output = null;
try {
output = Okio.sink(outputFile);
Okio.buffer(response.body().source()).readAll(output);
callback.onSuccess();
} finally {
if (output != null) {
output.close();
}
}
}
});
}
public void stopPollingOnChangeEndpoint() {
mOnChangePollingEnabled = false;
if (mRestartOnChangePollingHandler != null) {
mRestartOnChangePollingHandler.removeCallbacksAndMessages(null);
mRestartOnChangePollingHandler = null;
}
if (mOnChangePollingClient != null) {
mOnChangePollingClient.cancel(this);
mOnChangePollingClient = null;
}
mOnServerContentChangeListener = null;
}
public void startPollingOnChangeEndpoint(
OnServerContentChangeListener onServerContentChangeListener) {
if (mOnChangePollingEnabled) {
// polling already enabled
return;
}
mOnChangePollingEnabled = true;
mOnServerContentChangeListener = onServerContentChangeListener;
mOnChangePollingClient = new OkHttpClient();
mOnChangePollingClient
.setConnectionPool(new ConnectionPool(1, LONG_POLL_KEEP_ALIVE_DURATION_MS))
.setConnectTimeout(HTTP_CONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS);
mRestartOnChangePollingHandler = new Handler();
enqueueOnChangeEndpointLongPolling();
}
private void handleOnChangePollingResponse(boolean didServerContentChanged) {
if (mOnChangePollingEnabled) {
if (didServerContentChanged) {
UiThreadUtil.runOnUiThread(new Runnable() {
@Override
public void run() {
if (mOnServerContentChangeListener != null) {
mOnServerContentChangeListener.onServerContentChanged();
}
}
});
}
enqueueOnChangeEndpointLongPolling();
}
}
private void enqueueOnChangeEndpointLongPolling() {
Request request = new Request.Builder().url(createOnChangeEndpointUrl()).tag(this).build();
Assertions.assertNotNull(mOnChangePollingClient).newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Request request, IOException e) {
if (mOnChangePollingEnabled) {
// this runnable is used by onchange endpoint poller to delay subsequent requests in case
// of a failure, so that we don't flood network queue with frequent requests in case when
// dev server is down
FLog.d(ReactConstants.TAG, "Error while requesting /onchange endpoint", e);
Assertions.assertNotNull(mRestartOnChangePollingHandler).postDelayed(
new Runnable() {
@Override
public void run() {
handleOnChangePollingResponse(false);
}
},
LONG_POLL_FAILURE_DELAY_MS);
}
}
@Override
public void onResponse(Response response) throws IOException {
handleOnChangePollingResponse(response.code() == 205);
}
});
}
private String createOnChangeEndpointUrl() {
return String.format(Locale.US, ONCHANGE_ENDPOINT_URL_FORMAT, getDebugServerHost());
}
private String createLaunchChromeDevtoolsCommandUrl() {
return String.format(LAUNCH_CHROME_DEVTOOLS_COMMAND_URL_FORMAT, getDebugServerHost());
}
public void launchChromeDevtools() {
Request request = new Request.Builder()
.url(createLaunchChromeDevtoolsCommandUrl())
.build();
mClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Request request, IOException e) {
// ignore HTTP call response, this is just to open a debugger page and there is no reason
// to report failures from here
}
@Override
public void onResponse(Response response) throws IOException {
// ignore HTTP call response - see above
}
});
}
public String getSourceMapUrl(String mainModuleName) {
return String.format(Locale.US, SOURCE_MAP_URL_FORMAT, getDebugServerHost(), mainModuleName);
}
public String getSourceUrl(String mainModuleName) {
return String.format(Locale.US, BUNDLE_URL_FORMAT, getDebugServerHost(), mainModuleName);
}
public String getJSBundleURLForRemoteDebugging(String mainModuleName) {
// The host IP we use when connecting to the JS bundle server from the emulator is not the
// same as the one needed to connect to the same server from the Chrome proxy running on the
// host itself.
return createBundleURL(getHostForJSProxy(), mainModuleName);
}
}
@@ -0,0 +1,29 @@
/**
* 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.devsupport;
import android.os.Bundle;
import android.preference.PreferenceActivity;
import com.facebook.react.R;
/**
* Activity that display developers settings. Should be added to the debug manifest of the app. Can
* be triggered through the developers option menu displayed by {@link DevSupportManager}.
*/
public class DevSettingsActivity extends PreferenceActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setTitle(R.string.catalyst_settings_title);
addPreferencesFromResource(R.xml.preferences);
}
}
@@ -0,0 +1,629 @@
/**
* 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.devsupport;
import javax.annotation.Nullable;
import java.io.File;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Locale;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.hardware.SensorManager;
import android.os.Environment;
import android.view.WindowManager;
import android.widget.Toast;
import com.facebook.common.logging.FLog;
import com.facebook.infer.annotation.Assertions;
import com.facebook.react.R;
import com.facebook.react.bridge.CatalystInstance;
import com.facebook.react.bridge.NativeModuleCallExceptionHandler;
import com.facebook.react.bridge.ProxyJavaScriptExecutor;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.UiThreadUtil;
import com.facebook.react.bridge.WebsocketJavaScriptExecutor;
import com.facebook.react.common.ReactConstants;
import com.facebook.react.common.ShakeDetector;
import com.facebook.react.modules.debug.DeveloperSettings;
/**
* Interface for accessing and interacting with development features. Following features
* are supported through this manager class:
* 1) Displaying JS errors (aka RedBox)
* 2) Displaying developers menu (Reload JS, Debug JS)
* 3) Communication with developer server in order to download updated JS bundle
* 4) Starting/stopping broadcast receiver for js reload signals
* 5) Starting/stopping motion sensor listener that recognize shake gestures which in turn may
* trigger developers menu.
* 6) Launching developers settings view
*
* This class automatically monitors the state of registered views and activities to which they are
* bound to make sure that we don't display overlay or that we we don't listen for sensor events
* when app is backgrounded.
*
* {@link ReactInstanceDevCommandsHandler} implementation is responsible for instantiating this
* instance and for populating with an instance of {@link CatalystInstance} whenever instance
* manager recreates it (through {@link #onNewCatalystContextCreated}). Also, instance manager is
* responsible for enabling/disabling dev support in case when app is backgrounded or when all the
* views has been detached from the instance (through {@link #setDevSupportEnabled} method).
*
* IMPORTANT: In order for developer support to work correctly it is required that the
* manifest of your application contain the following entries:
* {@code <activity android:name="com.facebook.react.devsupport.DevSettingsActivity"/>}
* {@code <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>}
*/
public class DevSupportManager implements NativeModuleCallExceptionHandler {
private static final int JAVA_ERROR_COOKIE = -1;
private static final String JS_BUNDLE_FILE_NAME = "ReactNativeDevBundle.js";
private static final String EXOPACKAGE_LOCATION_FORMAT
= "/data/local/tmp/exopackage/%s//secondary-dex";
private final Context mApplicationContext;
private final ShakeDetector mShakeDetector;
private final BroadcastReceiver mReloadAppBroadcastReceiver;
private final DevServerHelper mDevServerHelper;
private final LinkedHashMap<String, DevOptionHandler> mCustomDevOptions =
new LinkedHashMap<>();
private final ReactInstanceDevCommandsHandler mReactInstanceCommandsHandler;
private final @Nullable String mJSAppBundleName;
private final File mJSBundleTempFile;
private @Nullable RedBoxDialog mRedBoxDialog;
private @Nullable AlertDialog mDevOptionsDialog;
private @Nullable DebugOverlayController mDebugOverlayController;
private @Nullable ReactContext mCurrentContext;
private DevInternalSettings mDevSettings;
private boolean mIsUsingJSProxy = false;
private boolean mIsReceiverRegistered = false;
private boolean mIsShakeDetectorStarted = false;
private boolean mIsDevSupportEnabled = false;
private boolean mIsCurrentlyProfiling = false;
private int mProfileIndex = 0;
public DevSupportManager(
Context applicationContext,
ReactInstanceDevCommandsHandler reactInstanceCommandsHandler,
@Nullable String packagerPathForJSBundleName,
boolean enableOnCreate) {
mReactInstanceCommandsHandler = reactInstanceCommandsHandler;
mApplicationContext = applicationContext;
mJSAppBundleName = packagerPathForJSBundleName;
mDevSettings = new DevInternalSettings(applicationContext, this);
mDevServerHelper = new DevServerHelper(mDevSettings);
// Prepare shake gesture detector (will be started/stopped from #reload)
mShakeDetector = new ShakeDetector(new ShakeDetector.ShakeListener() {
@Override
public void onShake() {
showDevOptionsDialog();
}
});
// Prepare reload APP broadcast receiver (will be registered/unregistered from #reload)
mReloadAppBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (DevServerHelper.getReloadAppAction(context).equals(action)) {
if (intent.getBooleanExtra(DevServerHelper.RELOAD_APP_EXTRA_JS_PROXY, false)) {
mIsUsingJSProxy = true;
mDevServerHelper.launchChromeDevtools();
} else {
mIsUsingJSProxy = false;
}
handleReloadJS();
}
}
};
// We store JS bundle loaded from dev server in a single destination in app's data dir.
// In case when someone schedule 2 subsequent reloads it may happen that JS thread will
// start reading first reload output while the second reload starts writing to the same
// file. As this should only be the case in dev mode we leave it as it is.
// TODO(6418010): Fix readers-writers problem in debug reload from HTTP server
mJSBundleTempFile = new File(applicationContext.getFilesDir(), JS_BUNDLE_FILE_NAME);
setDevSupportEnabled(enableOnCreate);
}
@Override
public void handleException(Exception e) {
if (mIsDevSupportEnabled) {
FLog.e(ReactConstants.TAG, "Exception in native call from JS", e);
CharSequence details = ExceptionFormatterHelper.javaStackTraceToHtml(e.getStackTrace());
showNewError(e.getMessage(), details, JAVA_ERROR_COOKIE);
} else {
if (e instanceof RuntimeException) {
// Because we are rethrowing the original exception, the original stacktrace will be
// preserved
throw (RuntimeException) e;
} else {
throw new RuntimeException(e);
}
}
}
/**
* Add option item to dev settings dialog displayed by this manager. In the case user select given
* option from that dialog, the appropriate handler passed as {@param optionHandler} will be
* called.
*/
public void addCustomDevOption(
String optionName,
DevOptionHandler optionHandler) {
mCustomDevOptions.put(optionName, optionHandler);
}
public void showNewJSError(String message, ReadableArray details, int errorCookie) {
showNewError(message, ExceptionFormatterHelper.jsStackTraceToHtml(details), errorCookie);
}
public void updateJSError(
final String message,
final ReadableArray details,
final int errorCookie) {
UiThreadUtil.runOnUiThread(
new Runnable() {
@Override
public void run() {
// Since we only show the first JS error in a succession of JS errors, make sure we only
// update the error message for that error message. This assumes that updateJSError
// belongs to the most recent showNewJSError
if (mRedBoxDialog == null ||
!mRedBoxDialog.isShowing() ||
errorCookie != mRedBoxDialog.getErrorCookie()) {
return;
}
mRedBoxDialog.setTitle(message);
mRedBoxDialog.setDetails(ExceptionFormatterHelper.jsStackTraceToHtml(details));
mRedBoxDialog.show();
}
});
}
private void showNewError(
final String message,
final CharSequence details,
final int errorCookie) {
UiThreadUtil.runOnUiThread(
new Runnable() {
@Override
public void run() {
if (mRedBoxDialog == null) {
mRedBoxDialog = new RedBoxDialog(mApplicationContext, DevSupportManager.this);
mRedBoxDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
}
if (mRedBoxDialog.isShowing()) {
// Sometimes errors cause multiple errors to be thrown in JS in quick succession. Only
// show the first and most actionable one.
return;
}
mRedBoxDialog.setTitle(message);
mRedBoxDialog.setDetails(details);
mRedBoxDialog.setErrorCookie(errorCookie);
mRedBoxDialog.show();
}
});
}
public void showDevOptionsDialog() {
if (mDevOptionsDialog != null || !mIsDevSupportEnabled) {
return;
}
LinkedHashMap<String, DevOptionHandler> options = new LinkedHashMap<>();
/* register standard options */
options.put(
mApplicationContext.getString(R.string.catalyst_reloadjs), new DevOptionHandler() {
@Override
public void onOptionSelected() {
handleReloadJS();
}
});
options.put(
mIsUsingJSProxy ?
mApplicationContext.getString(R.string.catalyst_debugjs_off) :
mApplicationContext.getString(R.string.catalyst_debugjs),
new DevOptionHandler() {
@Override
public void onOptionSelected() {
mIsUsingJSProxy = !mIsUsingJSProxy;
handleReloadJS();
}
});
options.put(
mApplicationContext.getString(R.string.catalyst_settings), new DevOptionHandler() {
@Override
public void onOptionSelected() {
Intent intent = new Intent(mApplicationContext, DevSettingsActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mApplicationContext.startActivity(intent);
}
});
options.put(
mApplicationContext.getString(R.string.catalyst_inspect_element),
new DevOptionHandler() {
@Override
public void onOptionSelected() {
mReactInstanceCommandsHandler.toggleElementInspector();
}
});
if (mCurrentContext != null &&
mCurrentContext.getCatalystInstance() != null &&
mCurrentContext.getCatalystInstance().getBridge() != null &&
mCurrentContext.getCatalystInstance().getBridge().supportsProfiling()) {
options.put(
mApplicationContext.getString(
mIsCurrentlyProfiling ? R.string.catalyst_stop_profile :
R.string.catalyst_start_profile),
new DevOptionHandler() {
@Override
public void onOptionSelected() {
if (mCurrentContext != null && mCurrentContext.hasActiveCatalystInstance()) {
if (mIsCurrentlyProfiling) {
mIsCurrentlyProfiling = false;
String profileName = (Environment.getExternalStorageDirectory().getPath() +
"/profile_" + mProfileIndex + ".json");
mProfileIndex++;
mCurrentContext.getCatalystInstance()
.getBridge()
.stopProfiler("profile", profileName);
Toast.makeText(
mCurrentContext,
"Profile output to " + profileName,
Toast.LENGTH_LONG).show();
} else {
mIsCurrentlyProfiling = true;
mCurrentContext.getCatalystInstance().getBridge().startProfiler("profile");
}
}
}
});
}
if (mCustomDevOptions.size() > 0) {
options.putAll(mCustomDevOptions);
}
final DevOptionHandler[] optionHandlers = options.values().toArray(new DevOptionHandler[0]);
mDevOptionsDialog = new AlertDialog.Builder(mApplicationContext)
.setItems(options.keySet().toArray(new String[0]), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
optionHandlers[which].onOptionSelected();
mDevOptionsDialog = null;
}
})
.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
mDevOptionsDialog = null;
}
})
.create();
mDevOptionsDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
mDevOptionsDialog.show();
}
/**
* {@link ReactInstanceDevCommandsHandler} is responsible for
* enabling/disabling dev support when a React view is attached/detached
* or when application state changes (e.g. the application is backgrounded).
*/
public void setDevSupportEnabled(boolean isDevSupportEnabled) {
mIsDevSupportEnabled = isDevSupportEnabled;
reload();
}
public boolean getDevSupportEnabled() {
return mIsDevSupportEnabled;
}
public DeveloperSettings getDevSettings() {
return mDevSettings;
}
public void onNewReactContextCreated(ReactContext reactContext) {
resetCurrentContext(reactContext);
}
public void onReactInstanceDestroyed(ReactContext reactContext) {
if (reactContext == mCurrentContext) {
// only call reset context when the destroyed context matches the one that is currently set
// for this manager
resetCurrentContext(null);
}
}
public String getSourceMapUrl() {
return mDevServerHelper.getSourceMapUrl(Assertions.assertNotNull(mJSAppBundleName));
}
public String getSourceUrl() {
return mDevServerHelper.getSourceUrl(Assertions.assertNotNull(mJSAppBundleName));
}
public String getJSBundleURLForRemoteDebugging() {
return mDevServerHelper.getJSBundleURLForRemoteDebugging(
Assertions.assertNotNull(mJSAppBundleName));
}
public String getDownloadedJSBundleFile() {
return mJSBundleTempFile.getAbsolutePath();
}
/**
* @return {@code true} if {@link ReactInstanceManager} should use downloaded JS bundle file
* instead of using JS file from assets. This may happen when app has not been updated since
* the last time we fetched the bundle.
*/
public boolean hasUpToDateJSBundleInCache() {
if (mIsDevSupportEnabled && mJSBundleTempFile.exists()) {
try {
String packageName = mApplicationContext.getPackageName();
PackageInfo thisPackage = mApplicationContext.getPackageManager()
.getPackageInfo(packageName, 0);
if (mJSBundleTempFile.lastModified() > thisPackage.lastUpdateTime) {
// Base APK has not been updated since we donwloaded JS, but if app is using exopackage
// it may only be a single dex that has been updated. We check for exopackage dir update
// time in that case.
File exopackageDir = new File(
String.format(Locale.US, EXOPACKAGE_LOCATION_FORMAT, packageName));
if (exopackageDir.exists()) {
return mJSBundleTempFile.lastModified() > exopackageDir.lastModified();
}
return true;
}
} catch (PackageManager.NameNotFoundException e) {
// Ignore this error and just fallback to loading JS from assets
FLog.e(ReactConstants.TAG, "DevSupport is unable to get current app info");
}
}
return false;
}
/**
* @return {@code true} if JS bundle {@param bundleAssetName} exists, in that case
* {@link ReactInstanceManager} should use that file from assets instead of downloading bundle
* from dev server
*/
public boolean hasBundleInAssets(String bundleAssetName) {
try {
String[] assets = mApplicationContext.getAssets().list("");
for (int i = 0; i < assets.length; i++) {
if (assets[i].equals(bundleAssetName)) {
return true;
}
}
} catch (IOException e) {
// Ignore this error and just fallback to downloading JS from devserver
FLog.e(ReactConstants.TAG, "Error while loading assets list");
}
return false;
}
private void resetCurrentContext(@Nullable ReactContext reactContext) {
if (mCurrentContext == reactContext) {
// new context is the same as the old one - do nothing
return;
}
// if currently profiling stop and write the profile file
if (mIsCurrentlyProfiling) {
mIsCurrentlyProfiling = false;
String profileName = (Environment.getExternalStorageDirectory().getPath() +
"/profile_" + mProfileIndex + ".json");
mProfileIndex++;
mCurrentContext.getCatalystInstance().getBridge().stopProfiler("profile", profileName);
}
mCurrentContext = reactContext;
// Recreate debug overlay controller with new CatalystInstance object
if (mDebugOverlayController != null) {
mDebugOverlayController.setFpsDebugViewVisible(false);
}
if (reactContext != null) {
mDebugOverlayController = new DebugOverlayController(reactContext);
}
reloadSettings();
}
/* package */ void reloadSettings() {
reload();
}
public void handleReloadJS() {
// dismiss redbox if exists
if (mRedBoxDialog != null) {
mRedBoxDialog.dismiss();
}
ProgressDialog progressDialog = new ProgressDialog(mApplicationContext);
progressDialog.setTitle(R.string.catalyst_jsload_title);
progressDialog.setMessage(mApplicationContext.getString(
mIsUsingJSProxy ? R.string.catalyst_remotedbg_message : R.string.catalyst_jsload_message));
progressDialog.setIndeterminate(true);
progressDialog.setCancelable(false);
progressDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
progressDialog.show();
if (mIsUsingJSProxy) {
reloadJSInProxyMode(progressDialog);
} else {
reloadJSFromServer(progressDialog);
}
}
private void reloadJSInProxyMode(final ProgressDialog progressDialog) {
// When using js proxy, there is no need to fetch JS bundle as proxy executor will do that
// anyway
mDevServerHelper.launchChromeDevtools();
final WebsocketJavaScriptExecutor webSocketJSExecutor = new WebsocketJavaScriptExecutor();
webSocketJSExecutor.connect(
mDevServerHelper.getWebsocketProxyURL(),
new WebsocketJavaScriptExecutor.JSExecutorConnectCallback() {
@Override
public void onSuccess() {
progressDialog.dismiss();
UiThreadUtil.runOnUiThread(
new Runnable() {
@Override
public void run() {
mReactInstanceCommandsHandler.onReloadWithJSDebugger(
new ProxyJavaScriptExecutor(webSocketJSExecutor));
}
});
}
@Override
public void onFailure(final Throwable cause) {
progressDialog.dismiss();
FLog.e(ReactConstants.TAG, "Unable to connect to remote debugger", cause);
UiThreadUtil.runOnUiThread(
new Runnable() {
@Override
public void run() {
showNewError(
mApplicationContext.getString(R.string.catalyst_remotedbg_error),
ExceptionFormatterHelper.javaStackTraceToHtml(cause.getStackTrace()),
JAVA_ERROR_COOKIE);
}
});
}
});
}
private void reloadJSFromServer(final ProgressDialog progressDialog) {
mDevServerHelper.downloadBundleFromURL(
new DevServerHelper.BundleDownloadCallback() {
@Override
public void onSuccess() {
progressDialog.dismiss();
UiThreadUtil.runOnUiThread(
new Runnable() {
@Override
public void run() {
mReactInstanceCommandsHandler.onJSBundleLoadedFromServer();
}
});
}
@Override
public void onFailure(final Exception cause) {
progressDialog.dismiss();
FLog.e(ReactConstants.TAG, "Unable to download JS bundle", cause);
UiThreadUtil.runOnUiThread(
new Runnable() {
@Override
public void run() {
if (cause instanceof DebugServerException) {
DebugServerException debugServerException = (DebugServerException) cause;
showNewError(
debugServerException.description,
ExceptionFormatterHelper.debugServerExcStackTraceToHtml(
(DebugServerException) cause),
JAVA_ERROR_COOKIE);
} else {
showNewError(
mApplicationContext.getString(R.string.catalyst_jsload_error),
ExceptionFormatterHelper.javaStackTraceToHtml(cause.getStackTrace()),
JAVA_ERROR_COOKIE);
}
}
});
}
},
Assertions.assertNotNull(mJSAppBundleName),
mJSBundleTempFile);
}
private void reload() {
// reload settings, show/hide debug overlay if required & start/stop shake detector
if (mIsDevSupportEnabled) {
// update visibility of FPS debug overlay depending on the settings
if (mDebugOverlayController != null) {
mDebugOverlayController.setFpsDebugViewVisible(mDevSettings.isFpsDebugEnabled());
}
// start shake gesture detector
if (!mIsShakeDetectorStarted) {
mShakeDetector.start(
(SensorManager) mApplicationContext.getSystemService(Context.SENSOR_SERVICE));
mIsShakeDetectorStarted = true;
}
// register reload app broadcast receiver
if (!mIsReceiverRegistered) {
IntentFilter filter = new IntentFilter();
filter.addAction(DevServerHelper.getReloadAppAction(mApplicationContext));
mApplicationContext.registerReceiver(mReloadAppBroadcastReceiver, filter);
mIsReceiverRegistered = true;
}
if (mDevSettings.isReloadOnJSChangeEnabled()) {
mDevServerHelper.startPollingOnChangeEndpoint(
new DevServerHelper.OnServerContentChangeListener() {
@Override
public void onServerContentChanged() {
handleReloadJS();
}
});
} else {
mDevServerHelper.stopPollingOnChangeEndpoint();
}
} else {
// hide FPS debug overlay
if (mDebugOverlayController != null) {
mDebugOverlayController.setFpsDebugViewVisible(false);
}
// stop shake gesture detector
if (mIsShakeDetectorStarted) {
mShakeDetector.stop();
mIsShakeDetectorStarted = false;
}
// unregister app reload broadcast receiver
if (mIsReceiverRegistered) {
mApplicationContext.unregisterReceiver(mReloadAppBroadcastReceiver);
mIsReceiverRegistered = false;
}
// hide redbox dialog
if (mRedBoxDialog != null) {
mRedBoxDialog.dismiss();
}
// hide dev options dialog
if (mDevOptionsDialog != null) {
mDevOptionsDialog.dismiss();
}
mDevServerHelper.stopPollingOnChangeEndpoint();
}
}
}
@@ -0,0 +1,75 @@
/**
* 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.devsupport;
import java.io.File;
import android.text.Html;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
/**
* Helper class for displaying errors in an eye-catching form (red box).
*/
/* package */ class ExceptionFormatterHelper {
private static String getStackTraceHtmlComponent(
String methodName, String filename, int lineNumber, int columnNumber) {
StringBuilder stringBuilder = new StringBuilder();
methodName = methodName.replace("<", "&lt;").replace(">", "&gt;");
stringBuilder.append("<font color=#FDE5E5>")
.append(methodName)
.append("</font><br /><font color=#F9B3B3>")
.append(filename)
.append(":")
.append(lineNumber);
if (columnNumber != -1) {
stringBuilder
.append(":")
.append(columnNumber);
}
stringBuilder.append("</font><br /><br />");
return stringBuilder.toString();
}
public static CharSequence jsStackTraceToHtml(ReadableArray stack) {
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < stack.size(); i++) {
ReadableMap frame = stack.getMap(i);
String methodName = frame.getString("methodName");
String fileName = new File(frame.getString("file")).getName();
int lineNumber = frame.getInt("lineNumber");
int columnNumber = -1;
if (frame.hasKey("column") && !frame.isNull("column")) {
columnNumber = frame.getInt("column");
}
stringBuilder.append(getStackTraceHtmlComponent(
methodName, fileName, lineNumber, columnNumber));
}
return Html.fromHtml(stringBuilder.toString());
}
public static CharSequence javaStackTraceToHtml(StackTraceElement[] stack) {
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i< stack.length; i++) {
stringBuilder.append(getStackTraceHtmlComponent(
stack[i].getMethodName(), stack[i].getFileName(), stack[i].getLineNumber(), -1));
}
return Html.fromHtml(stringBuilder.toString());
}
public static CharSequence debugServerExcStackTraceToHtml(DebugServerException e) {
String s = getStackTraceHtmlComponent("", e.fileName, e.lineNumber, e.column);
return Html.fromHtml(s);
}
}
@@ -0,0 +1,102 @@
/**
* 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.devsupport;
import java.util.Locale;
import android.annotation.TargetApi;
import android.view.Choreographer;
import android.widget.FrameLayout;
import android.widget.TextView;
import com.facebook.common.logging.FLog;
import com.facebook.react.R;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.common.ReactConstants;
import com.facebook.react.modules.debug.FpsDebugFrameCallback;
/**
* View that automatically monitors and displays the current app frame rate. Also logs the current
* FPS to logcat while active.
*
* NB: Requires API 16 for use of FpsDebugFrameCallback.
*/
@TargetApi(16)
public class FpsView extends FrameLayout {
private static final int UPDATE_INTERVAL_MS = 500;
private final TextView mTextView;
private final FpsDebugFrameCallback mFrameCallback;
private final FPSMonitorRunnable mFPSMonitorRunnable;
public FpsView(ReactContext reactContext) {
super(reactContext);
inflate(reactContext, R.layout.fps_view, this);
mTextView = (TextView) findViewById(R.id.fps_text);
mFrameCallback = new FpsDebugFrameCallback(Choreographer.getInstance(), reactContext);
mFPSMonitorRunnable = new FPSMonitorRunnable();
setCurrentFPS(0, 0);
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
mFrameCallback.reset();
mFrameCallback.start();
mFPSMonitorRunnable.start();
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
mFrameCallback.stop();
mFPSMonitorRunnable.stop();
}
private void setCurrentFPS(double currentFPS, double currentJSFPS) {
String fpsString = String.format(
Locale.US,
"UI FPS: %.1f\nJS FPS: %.1f",
currentFPS,
currentJSFPS);
mTextView.setText(fpsString);
FLog.d(ReactConstants.TAG, fpsString);
}
/**
* Timer that runs every UPDATE_INTERVAL_MS ms and updates the currently displayed FPS.
*/
private class FPSMonitorRunnable implements Runnable {
private boolean mShouldStop = false;
@Override
public void run() {
if (mShouldStop) {
return;
}
setCurrentFPS(mFrameCallback.getFPS(), mFrameCallback.getJSFPS());
mFrameCallback.reset();
postDelayed(this, UPDATE_INTERVAL_MS);
}
public void start() {
mShouldStop = false;
post(this);
}
public void stop() {
mShouldStop = true;
}
}
}
@@ -0,0 +1,34 @@
/**
* 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.devsupport;
import com.facebook.react.bridge.ProxyJavaScriptExecutor;
/**
* Interface used by {@link DevSupportManager} for requesting React instance recreation
* based on the option that user select in developers menu.
*/
public interface ReactInstanceDevCommandsHandler {
/**
* Request react instance recreation with JS debugging enabled.
*/
void onReloadWithJSDebugger(ProxyJavaScriptExecutor proxyExecutor);
/**
* Notify react instance manager about new JS bundle version downloaded from the server.
*/
void onJSBundleLoadedFromServer();
/**
* Request to toggle the react element inspector.
*/
void toggleElementInspector();
}
@@ -0,0 +1,84 @@
/**
* 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.devsupport;
import android.app.Dialog;
import android.content.Context;
import android.graphics.Typeface;
import android.text.method.ScrollingMovementMethod;
import android.view.KeyEvent;
import android.view.View;
import android.view.Window;
import android.widget.Button;
import android.widget.TextView;
import com.facebook.react.R;
/**
* Dialog for displaying JS errors in an eye-catching form (red box).
*/
/* package */ class RedBoxDialog extends Dialog {
private final DevSupportManager mDevSupportManager;
private TextView mTitle;
private TextView mDetails;
private Button mReloadJs;
private int mCookie = 0;
protected RedBoxDialog(Context context, DevSupportManager devSupportManager) {
super(context, R.style.Theme_Catalyst_RedBox);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.redbox_view);
mDevSupportManager = devSupportManager;
mTitle = (TextView) findViewById(R.id.catalyst_redbox_title);
mDetails = (TextView) findViewById(R.id.catalyst_redbox_details);
mDetails.setTypeface(Typeface.MONOSPACE);
mDetails.setHorizontallyScrolling(true);
mDetails.setMovementMethod(new ScrollingMovementMethod());
mReloadJs = (Button) findViewById(R.id.catalyst_redbox_reloadjs);
mReloadJs.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mDevSupportManager.handleReloadJS();
}
});
}
public void setTitle(String title) {
mTitle.setText(title);
}
public void setDetails(CharSequence details) {
mDetails.setText(details);
}
public void setErrorCookie(int cookie) {
mCookie = cookie;
}
public int getErrorCookie() {
return mCookie;
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_MENU) {
mDevSupportManager.showDevOptionsDialog();
return true;
}
return super.onKeyUp(keyCode, event);
}
}
@@ -0,0 +1,50 @@
/**
* 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.modules.common;
import com.facebook.common.logging.FLog;
import com.facebook.react.bridge.CatalystInstance;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.common.ReactConstants;
/**
* Cleans sensitive user data from native modules that implement the {@code Cleanable} interface.
* This is useful e.g. when a user logs out from an app.
*/
public class ModuleDataCleaner {
/**
* Indicates a module may contain sensitive user data and should be cleaned on logout.
*
* Types of data that should be cleaned:
* - Persistent data (disk) that may contain user information or content.
* - Retained (static) in-memory data that may contain user info or content.
*
* Note that the following types of modules do not need to be cleaned here:
* - Modules whose user data is kept in memory in non-static fields, assuming the app uses a
* separate instance for each viewer context.
* - Modules that remove all persistent data (temp files, etc) when the catalyst instance is
* destroyed. This is because logout implies that the instance is destroyed. Apps should enforce
* this.
*/
public interface Cleanable {
void clearSensitiveData();
}
public static void cleanDataFromModules(CatalystInstance catalystInstance) {
for (NativeModule nativeModule : catalystInstance.getNativeModules()) {
if (nativeModule instanceof Cleanable) {
FLog.d(ReactConstants.TAG, "Cleaning data from " + nativeModule.getName());
((Cleanable) nativeModule).clearSensitiveData();
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More