+" > $FBA_SRC/README.facebook
+
+echo "Writing README"
+
+echo "The source of truth for css-layout is: https://github.com/facebook/css-layout
+
+The code here should be kept in sync with GitHub.
+HEAD at the time this code was synced: https://github.com/facebook/css-layout/commit/$COMMIT_ID
+
+There is generated code in:
+ - README (this file)
+ - java/com/facebook/csslayout (this folder)
+ - javatests/com/facebook/csslayout
+
+The code was generated by running 'make' in the css-layout folder and copied to React Native.
+" > $FBA_SRC/README
+
+echo "Done."
+echo "Please run buck test //javatests/com/facebook/csslayout"
diff --git a/ReactAndroid/src/main/java/com/facebook/jni/Countable.java b/ReactAndroid/src/main/java/com/facebook/jni/Countable.java
new file mode 100644
index 00000000000000..75892f48ca638c
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/jni/Countable.java
@@ -0,0 +1,40 @@
+/**
+ * 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.jni;
+
+import com.facebook.proguard.annotations.DoNotStrip;
+
+/**
+ * A Java Object that has native memory allocated corresponding to this instance.
+ *
+ * NB: THREAD SAFETY (this comment also exists at Countable.cpp)
+ *
+ * {@link #dispose} deletes the corresponding native object on whatever thread the method is called
+ * on. In the common case when this is called by Countable#finalize(), this will be called on the
+ * system finalizer thread. If you manually call dispose on the Java object, the native object
+ * will be deleted synchronously on that thread.
+ */
+@DoNotStrip
+public class Countable {
+ // Private C++ instance
+ @DoNotStrip
+ private long mInstance = 0;
+
+ public Countable() {
+ Prerequisites.ensure();
+ }
+
+ public native void dispose();
+
+ protected void finalize() throws Throwable {
+ dispose();
+ super.finalize();
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/jni/CppException.java b/ReactAndroid/src/main/java/com/facebook/jni/CppException.java
new file mode 100644
index 00000000000000..3006da53a95c1d
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/jni/CppException.java
@@ -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.jni;
+
+import com.facebook.proguard.annotations.DoNotStrip;
+
+@DoNotStrip
+public class CppException extends RuntimeException {
+ @DoNotStrip
+ public CppException(String message) {
+ super(message);
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/jni/CppSystemErrorException.java b/ReactAndroid/src/main/java/com/facebook/jni/CppSystemErrorException.java
new file mode 100644
index 00000000000000..18f754bf474999
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/jni/CppSystemErrorException.java
@@ -0,0 +1,27 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+package com.facebook.jni;
+
+import com.facebook.proguard.annotations.DoNotStrip;
+
+@DoNotStrip
+public class CppSystemErrorException extends CppException {
+ int errorCode;
+
+ @DoNotStrip
+ public CppSystemErrorException(String message, int errorCode) {
+ super(message);
+ this.errorCode = errorCode;
+ }
+
+ public int getErrorCode() {
+ return errorCode;
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/jni/HybridData.java b/ReactAndroid/src/main/java/com/facebook/jni/HybridData.java
new file mode 100644
index 00000000000000..fcb4ca33623fdd
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/jni/HybridData.java
@@ -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.jni;
+
+import com.facebook.proguard.annotations.DoNotStrip;
+
+/**
+ * This object holds a native C++ member for hybrid Java/C++ objects.
+ *
+ * NB: THREAD SAFETY
+ *
+ * {@link #dispose} deletes the corresponding native object on whatever thread
+ * the method is called on. In the common case when this is called by
+ * HybridData#finalize(), this will be called on the system finalizer
+ * thread. If you manually call resetNative() on the Java object, the C++
+ * object will be deleted synchronously on that thread.
+ */
+@DoNotStrip
+public class HybridData {
+ // Private C++ instance
+ @DoNotStrip
+ private long mNativePointer = 0;
+
+ public HybridData() {
+ Prerequisites.ensure();
+ }
+
+ /**
+ * To explicitly delete the instance, call resetNative(). If the C++
+ * instance is referenced after this is called, a NullPointerException will
+ * be thrown. resetNative() may be called multiple times safely. Because
+ * {@link #finalize} calls resetNative, the instance will not leak if this is
+ * not called, but timing of deletion and the thread the C++ dtor is called
+ * on will be at the whim of the Java GC. If you want to control the thread
+ * and timing of the destructor, you should call resetNative() explicitly.
+ */
+ public native void resetNative();
+
+ protected void finalize() throws Throwable {
+ resetNative();
+ super.finalize();
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/jni/Prerequisites.java b/ReactAndroid/src/main/java/com/facebook/jni/Prerequisites.java
new file mode 100644
index 00000000000000..b669e546b15b26
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/jni/Prerequisites.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.facebook.jni;
+
+
+import com.facebook.soloader.SoLoader;
+
+
+import javax.microedition.khronos.egl.EGL10;
+import javax.microedition.khronos.egl.EGLConfig;
+import javax.microedition.khronos.egl.EGLContext;
+import javax.microedition.khronos.egl.EGLDisplay;
+
+public class Prerequisites {
+ private static final int EGL_OPENGL_ES2_BIT = 0x0004;
+
+ public static void ensure() {
+ SoLoader.loadLibrary("fbjni");
+ }
+
+ // Code is simplified version of getDetectedVersion()
+ // from cts/tests/tests/graphics/src/android/opengl/cts/OpenGlEsVersionTest.java
+ static public boolean supportsOpenGL20() {
+ EGL10 egl = (EGL10) EGLContext.getEGL();
+ EGLDisplay display = egl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
+ int[] numConfigs = new int[1];
+
+ if (egl.eglInitialize(display, null)) {
+ try {
+ if (egl.eglGetConfigs(display, null, 0, numConfigs)) {
+ EGLConfig[] configs = new EGLConfig[numConfigs[0]];
+ if (egl.eglGetConfigs(display, configs, numConfigs[0], numConfigs)) {
+ int[] value = new int[1];
+ for (int i = 0; i < numConfigs[0]; i++) {
+ if (egl.eglGetConfigAttrib(display, configs[i],
+ EGL10.EGL_RENDERABLE_TYPE, value)) {
+ if ((value[0] & EGL_OPENGL_ES2_BIT) == EGL_OPENGL_ES2_BIT) {
+ return true;
+ }
+ }
+ }
+ }
+ }
+ } finally {
+ egl.eglTerminate(display);
+ }
+ }
+ return false;
+ }
+}
+
diff --git a/ReactAndroid/src/main/java/com/facebook/jni/UnknownCppException.java b/ReactAndroid/src/main/java/com/facebook/jni/UnknownCppException.java
new file mode 100644
index 00000000000000..fa6e971f6d414d
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/jni/UnknownCppException.java
@@ -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.jni;
+
+import com.facebook.proguard.annotations.DoNotStrip;
+
+@DoNotStrip
+public class UnknownCppException extends CppException {
+ @DoNotStrip
+ public UnknownCppException() {
+ super("Unknown");
+ }
+
+ @DoNotStrip
+ public UnknownCppException(String message) {
+ super(message);
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/proguard/annotations/DoNotStrip.java b/ReactAndroid/src/main/java/com/facebook/proguard/annotations/DoNotStrip.java
new file mode 100644
index 00000000000000..86a3f2c3fb6e2b
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/proguard/annotations/DoNotStrip.java
@@ -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.proguard.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import static java.lang.annotation.RetentionPolicy.CLASS;
+
+/**
+ * Add this annotation to a class, method, or field to instruct Proguard to not strip it out.
+ *
+ * This is useful for methods called via reflection that could appear as unused to Proguard.
+ */
+@Target({ ElementType.TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.CONSTRUCTOR })
+@Retention(CLASS)
+public @interface DoNotStrip {
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/proguard/annotations/KeepGettersAndSetters.java b/ReactAndroid/src/main/java/com/facebook/proguard/annotations/KeepGettersAndSetters.java
new file mode 100644
index 00000000000000..11f4f32b9878ac
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/proguard/annotations/KeepGettersAndSetters.java
@@ -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.proguard.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import static java.lang.annotation.RetentionPolicy.CLASS;
+
+/**
+ * Add this annotation to a class, to keep all "void set*(***)" and get* methods.
+ *
+ * This is useful for classes that are controlled by animator-like classes that control
+ * various properties with reflection.
+ *
+ *
NOTE: This is not needed for Views because their getters and setters
+ * are automatically kept by the default Android SDK ProGuard config.
+ */
+@Target({ElementType.TYPE})
+@Retention(CLASS)
+public @interface KeepGettersAndSetters {
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/proguard/annotations/proguard_annotations.pro b/ReactAndroid/src/main/java/com/facebook/proguard/annotations/proguard_annotations.pro
new file mode 100644
index 00000000000000..b1ef5f7ce94678
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/proguard/annotations/proguard_annotations.pro
@@ -0,0 +1,15 @@
+# Keep our interfaces so they can be used by other ProGuard rules.
+# See http://sourceforge.net/p/proguard/bugs/466/
+-keep,allowobfuscation @interface com.facebook.proguard.annotations.DoNotStrip
+-keep,allowobfuscation @interface com.facebook.proguard.annotations.KeepGettersAndSetters
+
+# Do not strip any method/class that is annotated with @DoNotStrip
+-keep @com.facebook.proguard.annotations.DoNotStrip class *
+-keepclassmembers class * {
+ @com.facebook.proguard.annotations.DoNotStrip *;
+}
+
+-keepclassmembers @com.facebook.proguard.annotations.KeepGettersAndSetters class * {
+ void set*(***);
+ *** get*();
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/CompositeReactPackage.java b/ReactAndroid/src/main/java/com/facebook/react/CompositeReactPackage.java
new file mode 100644
index 00000000000000..8f78569131ddc7
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/CompositeReactPackage.java
@@ -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 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 createNativeModules(ReactApplicationContext reactContext) {
+ final Map 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> createJSModules() {
+ final Set> 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 createViewManagers(ReactApplicationContext reactContext) {
+ final Map viewManagerMap = new HashMap<>();
+ for (ReactPackage reactPackage: mChildReactPackages) {
+ for (ViewManager viewManager: reactPackage.createViewManagers(reactContext)) {
+ viewManagerMap.put(viewManager.getName(), viewManager);
+ }
+ }
+ return new ArrayList(viewManagerMap.values());
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/CoreModulesPackage.java b/ReactAndroid/src/main/java/com/facebook/react/CoreModulesPackage.java
new file mode 100644
index 00000000000000..df524cf917b13a
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/CoreModulesPackage.java
@@ -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 createNativeModules(
+ ReactApplicationContext catalystApplicationContext) {
+ return Arrays.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> createJSModules() {
+ return Arrays.asList(
+ DeviceEventManagerModule.RCTDeviceEventEmitter.class,
+ JSTimersExecution.class,
+ RCTEventEmitter.class,
+ AppRegistry.class,
+ ReactNative.class,
+ DebugComponentOwnershipModule.RCTDebugComponentOwnership.class);
+ }
+
+ @Override
+ public List createViewManagers(ReactApplicationContext reactContext) {
+ return new ArrayList<>(0);
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/LifecycleState.java b/ReactAndroid/src/main/java/com/facebook/react/LifecycleState.java
new file mode 100644
index 00000000000000..f8598e90884572
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/LifecycleState.java
@@ -0,0 +1,27 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+package com.facebook.react;
+
+/**
+ * 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,
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java b/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java
new file mode 100644
index 00000000000000..3cd5cb7351b424
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java
@@ -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 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 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 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 createAllViewManagers(
+ ReactApplicationContext catalystApplicationContext) {
+ List 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 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:
+ *
+ * - {@link #setApplication}
+ *
- {@link #setBundleAssetName} or {@link #setJSMainModuleName}
+ *
+ */
+ 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"));
+ }
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactPackage.java b/ReactAndroid/src/main/java/com/facebook/react/ReactPackage.java
new file mode 100644
index 00000000000000..f11c2408cf0642
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/ReactPackage.java
@@ -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 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> createJSModules();
+
+ /**
+ * @return a list of view managers that should be registered with {@link UIManagerModule}
+ */
+ List createViewManagers(ReactApplicationContext reactContext);
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java b/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java
new file mode 100644
index 00000000000000..9d6d1bfd1d56f0
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java
@@ -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);
+ }
+ }
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/animation/AbstractFloatPairPropertyUpdater.java b/ReactAndroid/src/main/java/com/facebook/react/animation/AbstractFloatPairPropertyUpdater.java
new file mode 100644
index 00000000000000..0ef9cc9e6a93bf
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/animation/AbstractFloatPairPropertyUpdater.java
@@ -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);
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/animation/AbstractSingleFloatProperyUpdater.java b/ReactAndroid/src/main/java/com/facebook/react/animation/AbstractSingleFloatProperyUpdater.java
new file mode 100644
index 00000000000000..e50bbfeaf402ca
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/animation/AbstractSingleFloatProperyUpdater.java
@@ -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);
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/animation/Animation.java b/ReactAndroid/src/main/java/com/facebook/react/animation/Animation.java
new file mode 100644
index 00000000000000..37a6d2a1969997
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/animation/Animation.java
@@ -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;
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/animation/AnimationListener.java b/ReactAndroid/src/main/java/com/facebook/react/animation/AnimationListener.java
new file mode 100644
index 00000000000000..a3678ed91ff26c
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/animation/AnimationListener.java
@@ -0,0 +1,27 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+package com.facebook.react.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();
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/animation/AnimationPropertyUpdater.java b/ReactAndroid/src/main/java/com/facebook/react/animation/AnimationPropertyUpdater.java
new file mode 100644
index 00000000000000..3cbdbf3327df85
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/animation/AnimationPropertyUpdater.java
@@ -0,0 +1,46 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+package com.facebook.react.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);
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/animation/AnimationRegistry.java b/ReactAndroid/src/main/java/com/facebook/react/animation/AnimationRegistry.java
new file mode 100644
index 00000000000000..74f0bedf47ef59
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/animation/AnimationRegistry.java
@@ -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 mRegistry = new SparseArray();
+
+ 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;
+ }
+
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/animation/ImmediateAnimation.java b/ReactAndroid/src/main/java/com/facebook/react/animation/ImmediateAnimation.java
new file mode 100644
index 00000000000000..da72250548190e
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/animation/ImmediateAnimation.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+package com.facebook.react.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();
+ }
+
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/animation/NoopAnimationPropertyUpdater.java b/ReactAndroid/src/main/java/com/facebook/react/animation/NoopAnimationPropertyUpdater.java
new file mode 100644
index 00000000000000..83dbe3d9aebc11
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/animation/NoopAnimationPropertyUpdater.java
@@ -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) {
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/animation/OpacityAnimationPropertyUpdater.java b/ReactAndroid/src/main/java/com/facebook/react/animation/OpacityAnimationPropertyUpdater.java
new file mode 100644
index 00000000000000..d1acf3da59174d
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/animation/OpacityAnimationPropertyUpdater.java
@@ -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);
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/animation/PositionAnimationPairPropertyUpdater.java b/ReactAndroid/src/main/java/com/facebook/react/animation/PositionAnimationPairPropertyUpdater.java
new file mode 100644
index 00000000000000..b90740c5172c25
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/animation/PositionAnimationPairPropertyUpdater.java
@@ -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());
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/animation/RotationAnimationPropertyUpdater.java b/ReactAndroid/src/main/java/com/facebook/react/animation/RotationAnimationPropertyUpdater.java
new file mode 100644
index 00000000000000..214c84f6671e11
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/animation/RotationAnimationPropertyUpdater.java
@@ -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));
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/animation/ScaleXAnimationPropertyUpdater.java b/ReactAndroid/src/main/java/com/facebook/react/animation/ScaleXAnimationPropertyUpdater.java
new file mode 100644
index 00000000000000..9eb5567557a048
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/animation/ScaleXAnimationPropertyUpdater.java
@@ -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);
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/animation/ScaleXYAnimationPairPropertyUpdater.java b/ReactAndroid/src/main/java/com/facebook/react/animation/ScaleXYAnimationPairPropertyUpdater.java
new file mode 100644
index 00000000000000..3ca9429d01928b
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/animation/ScaleXYAnimationPairPropertyUpdater.java
@@ -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]);
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/animation/ScaleYAnimationPropertyUpdater.java b/ReactAndroid/src/main/java/com/facebook/react/animation/ScaleYAnimationPropertyUpdater.java
new file mode 100644
index 00000000000000..25b02f2d0d9b09
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/animation/ScaleYAnimationPropertyUpdater.java
@@ -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);
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/Arguments.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/Arguments.java
new file mode 100644
index 00000000000000..15d498df12191c
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/Arguments.java
@@ -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:
+ *
+ *
+ * - primitive types: int, float, double, boolean
+ * - arrays supported by {@link #fromArray(Object)}
+ * - {@link Bundle} objects that are recursively converted to maps
+ *
+ *
+ * @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;
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/AssertionException.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/AssertionException.java
new file mode 100644
index 00000000000000..fa574cc3e93aa3
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/AssertionException.java
@@ -0,0 +1,22 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+package com.facebook.react.bridge;
+
+/**
+ * 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);
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/BaseJavaModule.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/BaseJavaModule.java
new file mode 100644
index 00000000000000..5e4760f7c74993
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/BaseJavaModule.java
@@ -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 getMethods() {
+ Map methods = new HashMap();
+ 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 getConstants() {
+ return null;
+ }
+
+ @Override
+ public final void writeConstantsField(JsonGenerator jg, String fieldName) throws IOException {
+ Map constants = getConstants();
+ if (constants == null || constants.isEmpty()) {
+ return;
+ }
+
+ jg.writeObjectFieldStart(fieldName);
+ for (Map.Entry constant : constants.entrySet()) {
+ JsonGeneratorHelper.writeObjectField(
+ jg,
+ constant.getKey(),
+ constant.getValue());
+ }
+ jg.writeEndObject();
+ }
+
+ @Override
+ public void initialize() {
+ // do nothing
+ }
+
+ @Override
+ public void onCatalystInstanceDestroy() {
+ // do nothing
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/Callback.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/Callback.java
new file mode 100644
index 00000000000000..ab72c46ba27565
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/Callback.java
@@ -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);
+
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/CallbackImpl.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/CallbackImpl.java
new file mode 100644
index 00000000000000..8b5153e5cc51eb
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/CallbackImpl.java
@@ -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));
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/CatalystInstance.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/CatalystInstance.java
new file mode 100644
index 00000000000000..be3cd5d0aabc6e
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/CatalystInstance.java
@@ -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 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();
+ 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, "");
+ 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 getJSModule(Class jsInterface) {
+ return Assertions.assertNotNull(mJSModuleRegistry).getJavaScriptModule(jsInterface);
+ }
+
+ public T getNativeModule(Class nativeModuleInterface) {
+ return mJavaRegistry.getModule(nativeModuleInterface);
+ }
+
+ public Collection getNativeModules() {
+ return mJavaRegistry.getAllModules();
+ }
+
+ /**
+ * Adds a idle listener for this Catalyst instance. The listener will receive notifications
+ * whenever the bridge transitions from idle to busy and vice-versa, where the busy state is
+ * defined as there being some non-zero number of calls to JS that haven't resolved via a
+ * onBatchCompleted call. The listener should be purely passive and not affect application logic.
+ */
+ 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));
+ }
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/GuardedAsyncTask.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/GuardedAsyncTask.java
new file mode 100644
index 00000000000000..917c1279c667bd
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/GuardedAsyncTask.java
@@ -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
+ extends AsyncTask {
+
+ 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);
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/InvalidIteratorException.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/InvalidIteratorException.java
new file mode 100644
index 00000000000000..eea4c1d0723bce
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/InvalidIteratorException.java
@@ -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);
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/JSApplicationCausedNativeException.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/JSApplicationCausedNativeException.java
new file mode 100644
index 00000000000000..38ad4a2f5cfa2b
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/JSApplicationCausedNativeException.java
@@ -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);
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/JSApplicationIllegalArgumentException.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/JSApplicationIllegalArgumentException.java
new file mode 100644
index 00000000000000..faf123e88920eb
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/JSApplicationIllegalArgumentException.java
@@ -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);
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/JSBundleLoader.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/JSBundleLoader.java
new file mode 100644
index 00000000000000..ee42c51531215f
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/JSBundleLoader.java
@@ -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);
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/JSCJavaScriptExecutor.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/JSCJavaScriptExecutor.java
new file mode 100644
index 00000000000000..d371cf5be62163
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/JSCJavaScriptExecutor.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+package com.facebook.react.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();
+
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/JSDebuggerWebSocketClient.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/JSDebuggerWebSocketClient.java
new file mode 100644
index 00000000000000..8940ffc0860deb
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/JSDebuggerWebSocketClient.java
@@ -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 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 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();
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/JavaScriptExecutor.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/JavaScriptExecutor.java
new file mode 100644
index 00000000000000..2bc5e26c5c53ac
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/JavaScriptExecutor.java
@@ -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() {
+ }
+
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/JavaScriptModule.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/JavaScriptModule.java
new file mode 100644
index 00000000000000..af23afcfe01e30
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/JavaScriptModule.java
@@ -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 {
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/JavaScriptModuleRegistration.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/JavaScriptModuleRegistration.java
new file mode 100644
index 00000000000000..5e20f0970531bd
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/JavaScriptModuleRegistration.java
@@ -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 mMethodsToIds;
+ private final Map 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() {
+ @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 getMethods() {
+ return mMethodsToIds.keySet();
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/JavaScriptModuleRegistry.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/JavaScriptModuleRegistry.java
new file mode 100644
index 00000000000000..fab0f231e339d1
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/JavaScriptModuleRegistry.java
@@ -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, 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 getJavaScriptModule(Class 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;
+ }
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/JavaScriptModulesConfig.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/JavaScriptModulesConfig.java
new file mode 100644
index 00000000000000..bc75da277911d1
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/JavaScriptModulesConfig.java
@@ -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 mModules;
+
+ private JavaScriptModulesConfig(List modules) {
+ mModules = modules;
+ }
+
+ /*package*/ List 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 mModules =
+ new ArrayList();
+
+ 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);
+ }
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/JsonGeneratorHelper.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/JsonGeneratorHelper.java
new file mode 100644
index 00000000000000..551ca5ac243909
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/JsonGeneratorHelper.java
@@ -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 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();
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/LifecycleEventListener.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/LifecycleEventListener.java
new file mode 100644
index 00000000000000..faecb9730c7489
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/LifecycleEventListener.java
@@ -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();
+
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeArgumentsParseException.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeArgumentsParseException.java
new file mode 100644
index 00000000000000..7efeb1476630cf
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeArgumentsParseException.java
@@ -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);
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeArray.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeArray.java
new file mode 100644
index 00000000000000..1091ce0bad1fc8
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeArray.java
@@ -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;
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeMap.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeMap.java
new file mode 100644
index 00000000000000..9b5ded014c87a4
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeMap.java
@@ -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();
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeModule.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeModule.java
new file mode 100644
index 00000000000000..02df61959747d4
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeModule.java
@@ -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 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();
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeModuleCallExceptionHandler.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeModuleCallExceptionHandler.java
new file mode 100644
index 00000000000000..708bdfd8d03899
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeModuleCallExceptionHandler.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+package com.facebook.react.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);
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeModuleRegistry.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeModuleRegistry.java
new file mode 100644
index 00000000000000..42a794ecae1ed7
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeModuleRegistry.java
@@ -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 mModuleTable;
+ private final Map, NativeModule> mModuleInstances;
+ private final String mModuleDescriptions;
+ private final ArrayList mBatchCompleteListenerModules;
+
+ private NativeModuleRegistry(
+ ArrayList moduleTable,
+ Map, NativeModule> moduleInstances,
+ String moduleDescriptions) {
+ mModuleTable = moduleTable;
+ mModuleInstances = moduleInstances;
+ mModuleDescriptions = moduleDescriptions;
+
+ mBatchCompleteListenerModules = new ArrayList(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 getModule(Class moduleInterface) {
+ return (T) Assertions.assertNotNull(mModuleInstances.get(moduleInterface));
+ }
+
+ public Collection getAllModules() {
+ return mModuleInstances.values();
+ }
+
+ private static class ModuleDefinition {
+ public final int id;
+ public final String name;
+ public final NativeModule target;
+ public final ArrayList methods;
+
+ public ModuleDefinition(int id, String name, NativeModule target) {
+ this.id = id;
+ this.name = name;
+ this.target = target;
+ this.methods = new ArrayList();
+
+ for (Map.Entry 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 mModuleDefinitions;
+ private Map, NativeModule> mModuleInstances;
+ private Set mSeenModuleNames;
+
+ public Builder() {
+ mModuleDefinitions = new ArrayList();
+ 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) 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);
+ }
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/NoSuchKeyException.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/NoSuchKeyException.java
new file mode 100644
index 00000000000000..4d5630f9fda36e
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/NoSuchKeyException.java
@@ -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);
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/NotThreadSafeBridgeIdleDebugListener.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/NotThreadSafeBridgeIdleDebugListener.java
new file mode 100644
index 00000000000000..aa571141c05c50
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/NotThreadSafeBridgeIdleDebugListener.java
@@ -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();
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/ObjectAlreadyConsumedException.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/ObjectAlreadyConsumedException.java
new file mode 100644
index 00000000000000..9b374c145ea852
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/ObjectAlreadyConsumedException.java
@@ -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);
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/OnBatchCompleteListener.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/OnBatchCompleteListener.java
new file mode 100644
index 00000000000000..25db113ae0d5a2
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/OnBatchCompleteListener.java
@@ -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();
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/ProxyJavaScriptExecutor.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/ProxyJavaScriptExecutor.java
new file mode 100644
index 00000000000000..08447802bf0b8f
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/ProxyJavaScriptExecutor.java
@@ -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);
+
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/ReactApplicationContext.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReactApplicationContext.java
new file mode 100644
index 00000000000000..ccf9f7998355a3
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReactApplicationContext.java
@@ -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());
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/ReactBridge.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReactBridge.java
new file mode 100644
index 00000000000000..137ca098ec2104
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReactBridge.java
@@ -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);
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/ReactCallback.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReactCallback.java
new file mode 100644
index 00000000000000..7e4376c5685156
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReactCallback.java
@@ -0,0 +1,22 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+package com.facebook.react.bridge;
+
+import com.facebook.proguard.annotations.DoNotStrip;
+
+@DoNotStrip
+public interface ReactCallback {
+
+ @DoNotStrip
+ void call(int moduleId, int methodId, ReadableNativeArray parameters);
+
+ @DoNotStrip
+ void onBatchComplete();
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/ReactContext.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReactContext.java
new file mode 100644
index 00000000000000..27363232b842cb
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReactContext.java
@@ -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 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 getJSModule(Class 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 getNativeModule(Class 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;
+ }
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/ReactContextBaseJavaModule.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReactContextBaseJavaModule.java
new file mode 100644
index 00000000000000..4d0470f46b865f
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReactContextBaseJavaModule.java
@@ -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;
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/ReactMethod.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReactMethod.java
new file mode 100644
index 00000000000000..0cc44f6e9ce49e
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReactMethod.java
@@ -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 {
+
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableArray.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableArray.java
new file mode 100644
index 00000000000000..47e5ed30cfeab1
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableArray.java
@@ -0,0 +1,27 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+package com.facebook.react.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);
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableMap.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableMap.java
new file mode 100644
index 00000000000000..5aa5adb43bcd7b
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableMap.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+package com.facebook.react.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();
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableMapKeySeyIterator.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableMapKeySeyIterator.java
new file mode 100644
index 00000000000000..3218611d3886ca
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableMapKeySeyIterator.java
@@ -0,0 +1,22 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+package com.facebook.react.bridge;
+
+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();
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableNativeArray.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableNativeArray.java
new file mode 100644
index 00000000000000..2dd03c3f87f9e7
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableNativeArray.java
@@ -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);
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableNativeMap.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableNativeMap.java
new file mode 100644
index 00000000000000..e2bfa848ef23c9
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableNativeMap.java
@@ -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);
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableType.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableType.java
new file mode 100644
index 00000000000000..0c6e2a04414d42
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableType.java
@@ -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,
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/SoftAssertions.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/SoftAssertions.java
new file mode 100644
index 00000000000000..f5d1f7a475d893
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/SoftAssertions.java
@@ -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 assertNotNull(@Nullable T instance) {
+ if (instance == null) {
+ throw new AssertionException("Expected object to not be null!");
+ }
+ return instance;
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/UiThreadUtil.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/UiThreadUtil.java
new file mode 100644
index 00000000000000..4fefe98a32373e
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/UiThreadUtil.java
@@ -0,0 +1,56 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+package com.facebook.react.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);
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/UnexpectedNativeTypeException.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/UnexpectedNativeTypeException.java
new file mode 100644
index 00000000000000..fee3ebbde7baf9
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/UnexpectedNativeTypeException.java
@@ -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);
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/WebsocketJavaScriptExecutor.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/WebsocketJavaScriptExecutor.java
new file mode 100644
index 00000000000000..4557d9fe674776
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/WebsocketJavaScriptExecutor.java
@@ -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 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);
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableArray.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableArray.java
new file mode 100644
index 00000000000000..6861669cb4844b
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableArray.java
@@ -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);
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableMap.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableMap.java
new file mode 100644
index 00000000000000..765fe39a584711
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableMap.java
@@ -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);
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableNativeArray.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableNativeArray.java
new file mode 100644
index 00000000000000..e10a0b57d4ec75
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableNativeArray.java
@@ -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);
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableNativeMap.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableNativeMap.java
new file mode 100644
index 00000000000000..c630a59b51a686
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableNativeMap.java
@@ -0,0 +1,68 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+package com.facebook.react.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);
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/package_js.py b/ReactAndroid/src/main/java/com/facebook/react/bridge/package_js.py
new file mode 100644
index 00000000000000..d874f5aa2ac5e6
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/package_js.py
@@ -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)
diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/queue/CatalystQueueConfiguration.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/queue/CatalystQueueConfiguration.java
new file mode 100644
index 00000000000000..10be2a44df103d
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/queue/CatalystQueueConfiguration.java
@@ -0,0 +1,90 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+package com.facebook.react.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 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);
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/queue/CatalystQueueConfigurationSpec.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/queue/CatalystQueueConfigurationSpec.java
new file mode 100644
index 00000000000000..c5eb9ad6859365
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/queue/CatalystQueueConfigurationSpec.java
@@ -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));
+ }
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/queue/MessageQueueThread.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/queue/MessageQueueThread.java
new file mode 100644
index 00000000000000..0090b82ad0bf58
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/queue/MessageQueueThread.java
@@ -0,0 +1,144 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+package com.facebook.react.bridge.queue;
+
+import android.os.Looper;
+
+import com.facebook.common.logging.FLog;
+import com.facebook.proguard.annotations.DoNotStrip;
+import com.facebook.react.bridge.AssertionException;
+import com.facebook.react.bridge.SoftAssertions;
+import com.facebook.react.common.ReactConstants;
+import com.facebook.react.common.futures.SimpleSettableFuture;
+
+/**
+ * Encapsulates a Thread that has a {@link Looper} running on it that can accept Runnables.
+ */
+@DoNotStrip
+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 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);
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/queue/MessageQueueThreadHandler.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/queue/MessageQueueThreadHandler.java
new file mode 100644
index 00000000000000..6350180fdb918c
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/queue/MessageQueueThreadHandler.java
@@ -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);
+ }
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/queue/MessageQueueThreadSpec.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/queue/MessageQueueThreadSpec.java
new file mode 100644
index 00000000000000..89673016cadd7a
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/queue/MessageQueueThreadSpec.java
@@ -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;
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/queue/NativeRunnable.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/queue/NativeRunnable.java
new file mode 100644
index 00000000000000..23eb266d4381e8
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/queue/NativeRunnable.java
@@ -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();
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/queue/QueueThreadExceptionHandler.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/queue/QueueThreadExceptionHandler.java
new file mode 100644
index 00000000000000..262f4aa5c47353
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/queue/QueueThreadExceptionHandler.java
@@ -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);
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/common/LongArray.java b/ReactAndroid/src/main/java/com/facebook/react/common/LongArray.java
new file mode 100644
index 00000000000000..74bced6cfa94e8
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/common/LongArray.java
@@ -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 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;
+ }
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/common/MapBuilder.java b/ReactAndroid/src/main/java/com/facebook/react/common/MapBuilder.java
new file mode 100644
index 00000000000000..f73fbdae352dd4
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/common/MapBuilder.java
@@ -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 HashMap newHashMap() {
+ return new HashMap();
+ }
+
+ /**
+ * Returns the empty map.
+ */
+ public static Map of() {
+ return newHashMap();
+ }
+
+ /**
+ * Returns map containing a single entry.
+ */
+ public static Map of(K k1, V v1) {
+ Map map = of();
+ map.put(k1, v1);
+ return map;
+ }
+
+ /**
+ * Returns map containing the given entries.
+ */
+ public static Map 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 Map 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 Map 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 Map 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 Map 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 Map 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 Builder builder() {
+ return new Builder();
+ }
+
+ public static final class Builder {
+
+ private Map mMap;
+ private boolean mUnderConstruction;
+
+ private Builder() {
+ mMap = newHashMap();
+ mUnderConstruction = true;
+ }
+
+ public Builder put(K k, V v) {
+ if (!mUnderConstruction) {
+ throw new IllegalStateException("Underlying map has already been built");
+ }
+ mMap.put(k,v);
+ return this;
+ }
+
+ public Map build() {
+ if (!mUnderConstruction) {
+ throw new IllegalStateException("Underlying map has already been built");
+ }
+ mUnderConstruction = false;
+ return mMap;
+ }
+ }
+
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/common/ReactConstants.java b/ReactAndroid/src/main/java/com/facebook/react/common/ReactConstants.java
new file mode 100644
index 00000000000000..9023f4150e3569
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/common/ReactConstants.java
@@ -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";
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/common/SetBuilder.java b/ReactAndroid/src/main/java/com/facebook/react/common/SetBuilder.java
new file mode 100644
index 00000000000000..6aa627eaf024e9
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/common/SetBuilder.java
@@ -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 HashSet newHashSet() {
+ return new HashSet();
+ }
+
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/common/ShakeDetector.java b/ReactAndroid/src/main/java/com/facebook/react/common/ShakeDetector.java
new file mode 100644
index 00000000000000..0197980e7fac24
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/common/ShakeDetector.java
@@ -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();
+ }
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/common/SystemClock.java b/ReactAndroid/src/main/java/com/facebook/react/common/SystemClock.java
new file mode 100644
index 00000000000000..29c31b416c5f75
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/common/SystemClock.java
@@ -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();
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/common/annotations/VisibleForTesting.java b/ReactAndroid/src/main/java/com/facebook/react/common/annotations/VisibleForTesting.java
new file mode 100644
index 00000000000000..f9a71ab1832119
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/common/annotations/VisibleForTesting.java
@@ -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 {
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/common/futures/SimpleSettableFuture.java b/ReactAndroid/src/main/java/com/facebook/react/common/futures/SimpleSettableFuture.java
new file mode 100644
index 00000000000000..a94a65cbc6e457
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/common/futures/SimpleSettableFuture.java
@@ -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 {
+
+ 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");
+ }
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/AndroidManifest.xml b/ReactAndroid/src/main/java/com/facebook/react/devsupport/AndroidManifest.xml
new file mode 100644
index 00000000000000..8e8524c731f514
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/AndroidManifest.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DebugOverlayController.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DebugOverlayController.java
new file mode 100644
index 00000000000000..00e075c509fdb7
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DebugOverlayController.java
@@ -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;
+ }
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DebugServerException.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DebugServerException.java
new file mode 100644
index 00000000000000..59f80aba467bea
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DebugServerException.java
@@ -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];
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevInternalSettings.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevInternalSettings.java
new file mode 100644
index 00000000000000..ca3558939d6bcb
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevInternalSettings.java
@@ -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);
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevOptionHandler.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevOptionHandler.java
new file mode 100644
index 00000000000000..48deca9b156753
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevOptionHandler.java
@@ -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();
+
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevServerHelper.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevServerHelper.java
new file mode 100644
index 00000000000000..46260652e7d778
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevServerHelper.java
@@ -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);
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSettingsActivity.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSettingsActivity.java
new file mode 100644
index 00000000000000..b9a70a79d1ce01
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSettingsActivity.java
@@ -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);
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManager.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManager.java
new file mode 100644
index 00000000000000..432b44ef01ffd0
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManager.java
@@ -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 }
+ * {@code }
+ */
+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 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 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();
+ }
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/ExceptionFormatterHelper.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/ExceptionFormatterHelper.java
new file mode 100644
index 00000000000000..89ae7d9bf4cc8f
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/ExceptionFormatterHelper.java
@@ -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("<", "<").replace(">", ">");
+ stringBuilder.append("")
+ .append(methodName)
+ .append("
")
+ .append(filename)
+ .append(":")
+ .append(lineNumber);
+ if (columnNumber != -1) {
+ stringBuilder
+ .append(":")
+ .append(columnNumber);
+ }
+ stringBuilder.append("
");
+ 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);
+ }
+
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/FpsView.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/FpsView.java
new file mode 100644
index 00000000000000..dfefbc10c572da
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/FpsView.java
@@ -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;
+ }
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/ReactInstanceDevCommandsHandler.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/ReactInstanceDevCommandsHandler.java
new file mode 100644
index 00000000000000..5489853b085f5f
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/ReactInstanceDevCommandsHandler.java
@@ -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();
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/RedBoxDialog.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/RedBoxDialog.java
new file mode 100644
index 00000000000000..1a3973a3dd0d5a
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/RedBoxDialog.java
@@ -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);
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/common/ModuleDataCleaner.java b/ReactAndroid/src/main/java/com/facebook/react/modules/common/ModuleDataCleaner.java
new file mode 100644
index 00000000000000..0e811e1a04817f
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/modules/common/ModuleDataCleaner.java
@@ -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();
+ }
+ }
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/core/DefaultHardwareBackBtnHandler.java b/ReactAndroid/src/main/java/com/facebook/react/modules/core/DefaultHardwareBackBtnHandler.java
new file mode 100644
index 00000000000000..55c2810bb1cd92
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/modules/core/DefaultHardwareBackBtnHandler.java
@@ -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.modules.core;
+
+/**
+ * Interface used by {@link DeviceEventManagerModule} to delegate hardware back button events. It's
+ * suppose to provide a default behavior since it would be triggered in the case when JS side
+ * doesn't want to handle back press events.
+ */
+public interface DefaultHardwareBackBtnHandler {
+
+ /**
+ * By default, all onBackPress() calls should not execute the default backpress handler and should
+ * instead propagate it to the JS instance. If JS doesn't want to handle the back press itself,
+ * it shall call back into native to invoke this function which should execute the default handler
+ */
+ void invokeDefaultOnBackPressed();
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/core/DeviceEventManagerModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/core/DeviceEventManagerModule.java
new file mode 100644
index 00000000000000..1329a5b7c6d8be
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/modules/core/DeviceEventManagerModule.java
@@ -0,0 +1,67 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+package com.facebook.react.modules.core;
+
+import javax.annotation.Nullable;
+
+import com.facebook.react.bridge.ReactApplicationContext;
+import com.facebook.react.bridge.ReactContext;
+import com.facebook.react.bridge.ReactContextBaseJavaModule;
+import com.facebook.react.bridge.ReactMethod;
+import com.facebook.react.bridge.JavaScriptModule;
+import com.facebook.react.bridge.UiThreadUtil;
+
+/**
+ * Native module that handles device hardware events like hardware back presses.
+ */
+public class DeviceEventManagerModule extends ReactContextBaseJavaModule {
+
+ public static interface RCTDeviceEventEmitter extends JavaScriptModule {
+ void emit(String eventName, @Nullable Object data);
+ }
+
+ private final Runnable mInvokeDefaultBackPressRunnable;
+
+ public DeviceEventManagerModule(
+ ReactApplicationContext reactContext,
+ final DefaultHardwareBackBtnHandler backBtnHandler) {
+ super(reactContext);
+ mInvokeDefaultBackPressRunnable = new Runnable() {
+ @Override
+ public void run() {
+ UiThreadUtil.assertOnUiThread();
+ backBtnHandler.invokeDefaultOnBackPressed();
+ }
+ };
+ }
+
+ /**
+ * Sends an event to the JS instance that the hardware back has been pressed.
+ */
+ public void emitHardwareBackPressed() {
+ getReactApplicationContext()
+ .getJSModule(RCTDeviceEventEmitter.class)
+ .emit("hardwareBackPress", null);
+ }
+
+ /**
+ * Invokes the default back handler for the host of this catalyst instance. This should be invoked
+ * if JS does not want to handle the back press itself.
+ */
+ @ReactMethod
+ public void invokeDefaultBackPressHandler() {
+ getReactApplicationContext().runOnUiQueueThread(mInvokeDefaultBackPressRunnable);
+ }
+
+ @Override
+ public String getName() {
+ return "DeviceEventManager";
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/core/ExceptionsManagerModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/core/ExceptionsManagerModule.java
new file mode 100644
index 00000000000000..87ca48b3928b21
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/modules/core/ExceptionsManagerModule.java
@@ -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.modules.core;
+
+import java.io.File;
+
+import com.facebook.common.logging.FLog;
+import com.facebook.react.bridge.BaseJavaModule;
+import com.facebook.react.bridge.ReactMethod;
+import com.facebook.react.bridge.ReadableArray;
+import com.facebook.react.bridge.ReadableMap;
+import com.facebook.react.devsupport.DevSupportManager;
+import com.facebook.react.common.ReactConstants;
+
+public class ExceptionsManagerModule extends BaseJavaModule {
+
+ private final DevSupportManager mDevSupportManager;
+
+ public ExceptionsManagerModule(DevSupportManager devSupportManager) {
+ mDevSupportManager = devSupportManager;
+ }
+
+ @Override
+ public String getName() {
+ return "RKExceptionsManager";
+ }
+
+ private String stackTraceToString(ReadableArray stack) {
+ StringBuilder stringBuilder = new StringBuilder();
+ for (int i = 0; i < stack.size(); i++) {
+ ReadableMap frame = stack.getMap(i);
+ stringBuilder.append(frame.getString("methodName"));
+ stringBuilder.append("\n ");
+ stringBuilder.append(new File(frame.getString("file")).getName());
+ stringBuilder.append(":");
+ stringBuilder.append(frame.getInt("lineNumber"));
+ if (frame.hasKey("column") && !frame.isNull("column")) {
+ stringBuilder
+ .append(":")
+ .append(frame.getInt("column"));
+ }
+ stringBuilder.append("\n");
+ }
+ return stringBuilder.toString();
+ }
+
+ @ReactMethod
+ public void reportFatalException(String title, ReadableArray details, int exceptionId) {
+ showOrThrowError(title, details, exceptionId);
+ }
+
+ @ReactMethod
+ public void reportSoftException(String title, ReadableArray details) {
+ FLog.e(ReactConstants.TAG, title + "\n" + stackTraceToString(details));
+ }
+
+ private void showOrThrowError(String title, ReadableArray details, int exceptionId) {
+ if (mDevSupportManager.getDevSupportEnabled()) {
+ mDevSupportManager.showNewJSError(title, details, exceptionId);
+ } else {
+ throw new JavascriptException(stackTraceToString(details));
+ }
+ }
+
+ @ReactMethod
+ public void updateExceptionMessage(String title, ReadableArray details, int exceptionId) {
+ if (mDevSupportManager.getDevSupportEnabled()) {
+ mDevSupportManager.updateJSError(title, details, exceptionId);
+ }
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/core/JSTimersExecution.java b/ReactAndroid/src/main/java/com/facebook/react/modules/core/JSTimersExecution.java
new file mode 100644
index 00000000000000..67f5ca2312f107
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/modules/core/JSTimersExecution.java
@@ -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.modules.core;
+
+import com.facebook.react.bridge.JavaScriptModule;
+import com.facebook.react.bridge.WritableArray;
+
+public interface JSTimersExecution extends JavaScriptModule {
+
+ public void callTimers(WritableArray timerIDs);
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/core/JavascriptException.java b/ReactAndroid/src/main/java/com/facebook/react/modules/core/JavascriptException.java
new file mode 100644
index 00000000000000..ef2fcb29d013a0
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/modules/core/JavascriptException.java
@@ -0,0 +1,21 @@
+/**
+ * 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.core;
+
+/**
+ * A JS exception that was propagated to native. In debug mode, these exceptions are normally shown
+ * to developers in a redbox.
+ */
+public class JavascriptException extends RuntimeException {
+
+ public JavascriptException(String jsStackTrace) {
+ super(jsStackTrace);
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/core/Timing.java b/ReactAndroid/src/main/java/com/facebook/react/modules/core/Timing.java
new file mode 100644
index 00000000000000..326b6c58e153c5
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/modules/core/Timing.java
@@ -0,0 +1,204 @@
+/**
+ * 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.core;
+
+import javax.annotation.Nullable;
+
+import java.util.Comparator;
+import java.util.PriorityQueue;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import android.util.SparseArray;
+import android.view.Choreographer;
+
+import com.facebook.react.bridge.Arguments;
+import com.facebook.react.bridge.ReactApplicationContext;
+import com.facebook.react.bridge.ReactContext;
+import com.facebook.react.bridge.ReactContextBaseJavaModule;
+import com.facebook.react.bridge.ReactMethod;
+import com.facebook.react.bridge.LifecycleEventListener;
+import com.facebook.react.bridge.WritableArray;
+import com.facebook.react.uimanager.ReactChoreographer;
+import com.facebook.react.common.SystemClock;
+import com.facebook.infer.annotation.Assertions;
+
+/**
+ * Native module for JS timer execution. Timers fire on frame boundaries.
+ */
+public final class Timing extends ReactContextBaseJavaModule implements LifecycleEventListener {
+
+ private static class Timer {
+
+ private final int mCallbackID;
+ private final boolean mRepeat;
+ private final int mInterval;
+ private long mTargetTime;
+
+ private Timer(int callbackID, long initialTargetTime, int duration, boolean repeat) {
+ mCallbackID = callbackID;
+ mTargetTime = initialTargetTime;
+ mInterval = duration;
+ mRepeat = repeat;
+ }
+ }
+
+ private class FrameCallback implements Choreographer.FrameCallback {
+
+ /**
+ * Calls all timers that have expired since the last time this frame callback was called.
+ */
+ @Override
+ public void doFrame(long frameTimeNanos) {
+ if (isPaused.get()) {
+ return;
+ }
+
+ long frameTimeMillis = frameTimeNanos / 1000000;
+ WritableArray timersToCall = null;
+ synchronized (mTimerGuard) {
+ while (!mTimers.isEmpty() && mTimers.peek().mTargetTime < frameTimeMillis) {
+ Timer timer = mTimers.poll();
+ if (timersToCall == null) {
+ timersToCall = Arguments.createArray();
+ }
+ timersToCall.pushInt(timer.mCallbackID);
+ if (timer.mRepeat) {
+ timer.mTargetTime = frameTimeMillis + timer.mInterval;
+ mTimers.add(timer);
+ } else {
+ mTimerIdsToTimers.remove(timer.mCallbackID);
+ }
+ }
+ }
+
+ if (timersToCall != null) {
+ Assertions.assertNotNull(mJSTimersModule).callTimers(timersToCall);
+ }
+
+ mReactChoreographer.postFrameCallback(ReactChoreographer.CallbackType.TIMERS_EVENTS, this);
+ }
+ }
+
+ private final Object mTimerGuard = new Object();
+ private final PriorityQueue mTimers;
+ private final SparseArray mTimerIdsToTimers;
+ private final AtomicBoolean isPaused = new AtomicBoolean(false);
+ private final ReactChoreographer mReactChoreographer;
+ private final FrameCallback mFrameCallback = new FrameCallback();
+ private @Nullable JSTimersExecution mJSTimersModule;
+ private boolean mFrameCallbackPosted = false;
+
+ public Timing(ReactApplicationContext reactContext) {
+ super(reactContext);
+ mReactChoreographer = ReactChoreographer.getInstance();
+ // We store timers sorted by finish time.
+ mTimers = new PriorityQueue(
+ 11, // Default capacity: for some reason they don't expose a (Comparator) constructor
+ new Comparator() {
+ @Override
+ public int compare(Timer lhs, Timer rhs) {
+ long diff = lhs.mTargetTime - rhs.mTargetTime;
+ if (diff == 0) {
+ return 0;
+ } else if (diff < 0) {
+ return -1;
+ } else {
+ return 1;
+ }
+ }
+ });
+ mTimerIdsToTimers = new SparseArray();
+ }
+
+ @Override
+ public void initialize() {
+ mJSTimersModule = getReactApplicationContext().getCatalystInstance()
+ .getJSModule(JSTimersExecution.class);
+ getReactApplicationContext().addLifecycleEventListener(this);
+ setChoreographerCallback();
+ }
+
+ @Override
+ public void onHostPause() {
+ isPaused.set(true);
+ clearChoreographerCallback();
+ }
+
+ @Override
+ public void onHostDestroy() {
+ clearChoreographerCallback();
+ }
+
+ @Override
+ public void onHostResume() {
+ isPaused.set(false);
+ // TODO(5195192) Investigate possible problems related to restarting all tasks at the same
+ // moment
+ setChoreographerCallback();
+ }
+
+ @Override
+ public void onCatalystInstanceDestroy() {
+ clearChoreographerCallback();
+ }
+
+ private void setChoreographerCallback() {
+ if (!mFrameCallbackPosted) {
+ mReactChoreographer.postFrameCallback(
+ ReactChoreographer.CallbackType.TIMERS_EVENTS,
+ mFrameCallback);
+ mFrameCallbackPosted = true;
+ }
+ }
+
+ private void clearChoreographerCallback() {
+ if (mFrameCallbackPosted) {
+ mReactChoreographer.removeFrameCallback(
+ ReactChoreographer.CallbackType.TIMERS_EVENTS,
+ mFrameCallback);
+ mFrameCallbackPosted = false;
+ }
+ }
+
+ @Override
+ public String getName() {
+ return "RKTiming";
+ }
+
+ @ReactMethod
+ public void createTimer(
+ final int callbackID,
+ final int duration,
+ final double jsSchedulingTime,
+ final boolean repeat) {
+ // Adjust for the amount of time it took for native to receive the timer registration call
+ long adjustedDuration = (long) Math.max(
+ 0,
+ jsSchedulingTime - SystemClock.currentTimeMillis() + duration);
+ long initialTargetTime = SystemClock.nanoTime() / 1000000 + adjustedDuration;
+ Timer timer = new Timer(callbackID, initialTargetTime, duration, repeat);
+ synchronized (mTimerGuard) {
+ mTimers.add(timer);
+ mTimerIdsToTimers.put(callbackID, timer);
+ }
+ }
+
+ @ReactMethod
+ public void deleteTimer(int timerId) {
+ synchronized (mTimerGuard) {
+ Timer timer = mTimerIdsToTimers.get(timerId);
+ if (timer != null) {
+ // We may have already called/removed it
+ mTimerIdsToTimers.remove(timerId);
+ mTimers.remove(timer);
+ }
+ }
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/debug/AnimationsDebugModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/debug/AnimationsDebugModule.java
new file mode 100644
index 00000000000000..6b914aae269055
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/modules/debug/AnimationsDebugModule.java
@@ -0,0 +1,120 @@
+/**
+ * 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.debug;
+
+import javax.annotation.Nullable;
+
+import java.util.Locale;
+
+import android.os.Build;
+import android.view.Choreographer;
+import android.widget.Toast;
+
+import com.facebook.common.logging.FLog;
+import com.facebook.react.bridge.JSApplicationCausedNativeException;
+import com.facebook.react.bridge.ReactApplicationContext;
+import com.facebook.react.bridge.ReactContextBaseJavaModule;
+import com.facebook.react.bridge.ReactMethod;
+import com.facebook.react.common.ReactConstants;
+
+/**
+ * Module that records debug information during transitions (animated navigation events such as
+ * going from one screen to another).
+ */
+public class AnimationsDebugModule extends ReactContextBaseJavaModule {
+
+ private @Nullable FpsDebugFrameCallback mFrameCallback;
+ private final DeveloperSettings mCatalystSettings;
+
+ public AnimationsDebugModule(
+ ReactApplicationContext reactContext,
+ DeveloperSettings catalystSettings) {
+ super(reactContext);
+ mCatalystSettings = catalystSettings;
+ }
+
+ @Override
+ public String getName() {
+ return "AnimationsDebugModule";
+ }
+
+ @ReactMethod
+ public void startRecordingFps() {
+ if (!mCatalystSettings.isAnimationFpsDebugEnabled()) {
+ return;
+ }
+
+ if (mFrameCallback != null) {
+ throw new JSApplicationCausedNativeException("Already recording FPS!");
+ }
+ checkAPILevel();
+
+ mFrameCallback = new FpsDebugFrameCallback(
+ Choreographer.getInstance(),
+ getReactApplicationContext());
+ mFrameCallback.startAndRecordFpsAtEachFrame();
+ }
+
+ /**
+ * Called when an animation finishes. The caller should include the animation stop time in ms
+ * (unix time) so that we know when the animation stopped from the JS perspective and we don't
+ * count time after as being part of the animation.
+ */
+ @ReactMethod
+ public void stopRecordingFps(double animationStopTimeMs) {
+ if (mFrameCallback == null) {
+ return;
+ }
+ checkAPILevel();
+
+ mFrameCallback.stop();
+
+ // Casting to long is safe here since animationStopTimeMs is unix time and thus relatively small
+ FpsDebugFrameCallback.FpsInfo fpsInfo = mFrameCallback.getFpsInfo((long) animationStopTimeMs);
+
+ if (fpsInfo == null) {
+ Toast.makeText(getReactApplicationContext(), "Unable to get FPS info", Toast.LENGTH_LONG);
+ } else {
+ String fpsString = String.format(
+ Locale.US,
+ "FPS: %.2f, %d frames (%d expected)",
+ fpsInfo.fps,
+ fpsInfo.totalFrames,
+ fpsInfo.totalExpectedFrames);
+ String jsFpsString = String.format(
+ Locale.US,
+ "JS FPS: %.2f, %d frames (%d expected)",
+ fpsInfo.jsFps,
+ fpsInfo.totalJsFrames,
+ fpsInfo.totalExpectedFrames);
+ String debugString = fpsString + "\n" + jsFpsString + "\n" +
+ "Total Time MS: " + String.format(Locale.US, "%d", fpsInfo.totalTimeMs);
+ FLog.d(ReactConstants.TAG, debugString);
+ Toast.makeText(getReactApplicationContext(), debugString, Toast.LENGTH_LONG).show();
+ }
+
+ mFrameCallback = null;
+ }
+
+ @Override
+ public void onCatalystInstanceDestroy() {
+ if (mFrameCallback != null) {
+ mFrameCallback.stop();
+ mFrameCallback = null;
+ }
+ }
+
+ private static void checkAPILevel() {
+ if (Build.VERSION.SDK_INT < 16) {
+ throw new JSApplicationCausedNativeException(
+ "Animation debugging is not supported in API <16");
+ }
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/debug/DeveloperSettings.java b/ReactAndroid/src/main/java/com/facebook/react/modules/debug/DeveloperSettings.java
new file mode 100644
index 00000000000000..2ecad91c3c0be3
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/modules/debug/DeveloperSettings.java
@@ -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.modules.debug;
+
+/**
+ * Provides access to React Native developers settings.
+ */
+public interface DeveloperSettings {
+
+ /**
+ * @return whether an overlay showing current FPS should be shown.
+ */
+ boolean isFpsDebugEnabled();
+
+ /**
+ * @return Whether debug information about transitions should be displayed.
+ */
+ boolean isAnimationFpsDebugEnabled();
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/debug/DidJSUpdateUiDuringFrameDetector.java b/ReactAndroid/src/main/java/com/facebook/react/modules/debug/DidJSUpdateUiDuringFrameDetector.java
new file mode 100644
index 00000000000000..28144b9afaf224
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/modules/debug/DidJSUpdateUiDuringFrameDetector.java
@@ -0,0 +1,175 @@
+/**
+ * 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.debug;
+
+import android.view.Choreographer;
+
+import com.facebook.react.bridge.ReactBridge;
+import com.facebook.react.bridge.NotThreadSafeBridgeIdleDebugListener;
+import com.facebook.react.common.LongArray;
+import com.facebook.react.uimanager.UIManagerModule;
+import com.facebook.react.uimanager.debug.NotThreadSafeUiManagerDebugListener;
+
+/**
+ * Debug object that listens to bridge busy/idle events and UiManagerModule dispatches and uses it
+ * to calculate whether JS was able to update the UI during a given frame. After being installed
+ * on a {@link ReactBridge} and a {@link UIManagerModule},
+ * {@link #getDidJSHitFrameAndCleanup} should be called once per frame via a
+ * {@link Choreographer.FrameCallback}.
+ */
+public class DidJSUpdateUiDuringFrameDetector implements NotThreadSafeBridgeIdleDebugListener,
+ NotThreadSafeUiManagerDebugListener {
+
+ private final LongArray mTransitionToIdleEvents = LongArray.createWithInitialCapacity(20);
+ private final LongArray mTransitionToBusyEvents = LongArray.createWithInitialCapacity(20);
+ private final LongArray mViewHierarchyUpdateEnqueuedEvents =
+ LongArray.createWithInitialCapacity(20);
+ private final LongArray mViewHierarchyUpdateFinishedEvents =
+ LongArray.createWithInitialCapacity(20);
+ private volatile boolean mWasIdleAtEndOfLastFrame = true;
+
+ @Override
+ public synchronized void onTransitionToBridgeIdle() {
+ mTransitionToIdleEvents.add(System.nanoTime());
+ }
+
+ @Override
+ public synchronized void onTransitionToBridgeBusy() {
+ mTransitionToBusyEvents.add(System.nanoTime());
+ }
+
+ @Override
+ public synchronized void onViewHierarchyUpdateEnqueued() {
+ mViewHierarchyUpdateEnqueuedEvents.add(System.nanoTime());
+ }
+
+ @Override
+ public synchronized void onViewHierarchyUpdateFinished() {
+ mViewHierarchyUpdateFinishedEvents.add(System.nanoTime());
+ }
+
+ /**
+ * Designed to be called from a {@link Choreographer.FrameCallback#doFrame} call.
+ *
+ * There are two 'success' cases that will cause {@link #getDidJSHitFrameAndCleanup} to
+ * return true for a given frame:
+ *
+ * 1) UIManagerModule finished dispatching a batched UI update on the UI thread during the frame.
+ * This means that during the next hierarchy traversal, new UI will be drawn if needed (good).
+ * 2) The bridge ended the frame idle (meaning there were no JS nor native module calls still in
+ * flight) AND there was no UiManagerModule update enqueued that didn't also finish. NB: if
+ * there was one enqueued that actually finished, we'd have case 1), so effectively we just
+ * look for whether one was enqueued.
+ *
+ * NB: This call can only be called once for a given frame time range because it cleans up
+ * events it recorded for that frame.
+ *
+ * NB2: This makes the assumption that onViewHierarchyUpdateEnqueued is called from the
+ * {@link UIManagerModule#onBatchComplete()}, e.g. while the bridge is still considered busy,
+ * which means there is no race condition where the bridge has gone idle but a hierarchy update is
+ * waiting to be enqueued.
+ *
+ * @param frameStartTimeNanos the time in nanos that the last frame started
+ * @param frameEndTimeNanos the time in nanos that the last frame ended
+ */
+ public synchronized boolean getDidJSHitFrameAndCleanup(
+ long frameStartTimeNanos,
+ long frameEndTimeNanos) {
+ // Case 1: We dispatched a UI update
+ boolean finishedUiUpdate = hasEventBetweenTimestamps(
+ mViewHierarchyUpdateFinishedEvents,
+ frameStartTimeNanos,
+ frameEndTimeNanos);
+ boolean didEndFrameIdle = didEndFrameIdle(frameStartTimeNanos, frameEndTimeNanos);
+
+ boolean hitFrame;
+ if (finishedUiUpdate) {
+ hitFrame = true;
+ } else {
+ // Case 2: Ended idle but no UI was enqueued during that frame
+ hitFrame = didEndFrameIdle && !hasEventBetweenTimestamps(
+ mViewHierarchyUpdateEnqueuedEvents,
+ frameStartTimeNanos,
+ frameEndTimeNanos);
+ }
+
+ cleanUp(mTransitionToIdleEvents, frameEndTimeNanos);
+ cleanUp(mTransitionToBusyEvents, frameEndTimeNanos);
+ cleanUp(mViewHierarchyUpdateEnqueuedEvents, frameEndTimeNanos);
+ cleanUp(mViewHierarchyUpdateFinishedEvents, frameEndTimeNanos);
+
+ mWasIdleAtEndOfLastFrame = didEndFrameIdle;
+
+ return hitFrame;
+ }
+
+ private static boolean hasEventBetweenTimestamps(
+ LongArray eventArray,
+ long startTime,
+ long endTime) {
+ for (int i = 0; i < eventArray.size(); i++) {
+ long time = eventArray.get(i);
+ if (time >= startTime && time < endTime) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static long getLastEventBetweenTimestamps(
+ LongArray eventArray,
+ long startTime,
+ long endTime) {
+ long lastEvent = -1;
+ for (int i = 0; i < eventArray.size(); i++) {
+ long time = eventArray.get(i);
+ if (time >= startTime && time < endTime) {
+ lastEvent = time;
+ } else if (time >= endTime) {
+ break;
+ }
+ }
+ return lastEvent;
+ }
+
+ private boolean didEndFrameIdle(long startTime, long endTime) {
+ long lastIdleTransition = getLastEventBetweenTimestamps(
+ mTransitionToIdleEvents,
+ startTime,
+ endTime);
+ long lastBusyTransition = getLastEventBetweenTimestamps(
+ mTransitionToBusyEvents,
+ startTime,
+ endTime);
+
+ if (lastIdleTransition == -1 && lastBusyTransition == -1) {
+ return mWasIdleAtEndOfLastFrame;
+ }
+
+ return lastIdleTransition > lastBusyTransition;
+ }
+
+ private static void cleanUp(LongArray eventArray, long endTime) {
+ int size = eventArray.size();
+ int indicesToRemove = 0;
+ for (int i = 0; i < size; i++) {
+ if (eventArray.get(i) < endTime) {
+ indicesToRemove++;
+ }
+ }
+
+ if (indicesToRemove > 0) {
+ for (int i = 0; i < size - indicesToRemove; i++) {
+ eventArray.set(i, eventArray.get(i + indicesToRemove));
+ }
+ eventArray.dropTail(indicesToRemove);
+ }
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/debug/FpsDebugFrameCallback.java b/ReactAndroid/src/main/java/com/facebook/react/modules/debug/FpsDebugFrameCallback.java
new file mode 100644
index 00000000000000..1e63f525df73e6
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/modules/debug/FpsDebugFrameCallback.java
@@ -0,0 +1,196 @@
+/**
+ * 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.debug;
+
+import javax.annotation.Nullable;
+
+import java.util.Map;
+import java.util.TreeMap;
+
+import android.annotation.TargetApi;
+import android.view.Choreographer;
+
+import com.facebook.react.bridge.ReactContext;
+import com.facebook.react.uimanager.UIManagerModule;
+import com.facebook.infer.annotation.Assertions;
+
+/**
+ * Each time a frame is drawn, records whether it should have expected any more callbacks since
+ * the last time a frame was drawn (i.e. was a frame skipped?). Uses this plus total elapsed time
+ * to determine FPS. Can also record total and expected frame counts, though NB, since the expected
+ * frame rate is estimated, the expected frame count will lose accuracy over time.
+ *
+ * Also records the JS FPS, i.e. the frames per second with which either JS updated the UI or was
+ * idle and not trying to update the UI. This is different from the FPS above since JS rendering is
+ * async.
+ *
+ * TargetApi 16 for use of Choreographer.
+ */
+@TargetApi(16)
+public class FpsDebugFrameCallback implements Choreographer.FrameCallback {
+
+ public static class FpsInfo {
+
+ public final int totalFrames;
+ public final int totalJsFrames;
+ public final int totalExpectedFrames;
+ public final double fps;
+ public final double jsFps;
+ public final int totalTimeMs;
+
+ public FpsInfo(
+ int totalFrames,
+ int totalJsFrames,
+ int totalExpectedFrames,
+ double fps,
+ double jsFps,
+ int totalTimeMs) {
+ this.totalFrames = totalFrames;
+ this.totalJsFrames = totalJsFrames;
+ this.totalExpectedFrames = totalExpectedFrames;
+ this.fps = fps;
+ this.jsFps = jsFps;
+ this.totalTimeMs = totalTimeMs;
+ }
+ }
+
+ private static final double EXPECTED_FRAME_TIME = 16.9;
+
+ private final Choreographer mChoreographer;
+ private final ReactContext mReactContext;
+ private final UIManagerModule mUIManagerModule;
+ private final DidJSUpdateUiDuringFrameDetector mDidJSUpdateUiDuringFrameDetector;
+
+ private boolean mShouldStop = false;
+ private long mFirstFrameTime = -1;
+ private long mLastFrameTime = -1;
+ private int mNumFrameCallbacks = 0;
+ private int mNumFrameCallbacksWithBatchDispatches = 0;
+ private boolean mIsRecordingFpsInfoAtEachFrame = false;
+ private @Nullable TreeMap mTimeToFps;
+
+ public FpsDebugFrameCallback(Choreographer choreographer, ReactContext reactContext) {
+ mChoreographer = choreographer;
+ mReactContext = reactContext;
+ mUIManagerModule = reactContext.getNativeModule(UIManagerModule.class);
+ mDidJSUpdateUiDuringFrameDetector = new DidJSUpdateUiDuringFrameDetector();
+ }
+
+ @Override
+ public void doFrame(long l) {
+ if (mShouldStop) {
+ return;
+ }
+
+ if (mFirstFrameTime == -1) {
+ mFirstFrameTime = l;
+ }
+
+ long lastFrameStartTime = mLastFrameTime;
+ mLastFrameTime = l;
+
+ if (mDidJSUpdateUiDuringFrameDetector.getDidJSHitFrameAndCleanup(
+ lastFrameStartTime,
+ l)) {
+ mNumFrameCallbacksWithBatchDispatches++;
+ }
+
+ mNumFrameCallbacks++;
+
+ if (mIsRecordingFpsInfoAtEachFrame) {
+ Assertions.assertNotNull(mTimeToFps);
+ FpsInfo info = new FpsInfo(
+ getNumFrames(),
+ getNumJSFrames(),
+ getExpectedNumFrames(),
+ getFPS(),
+ getJSFPS(),
+ getTotalTimeMS());
+ mTimeToFps.put(System.currentTimeMillis(), info);
+ }
+
+ mChoreographer.postFrameCallback(this);
+ }
+
+ public void start() {
+ mShouldStop = false;
+ mReactContext.getCatalystInstance().addBridgeIdleDebugListener(
+ mDidJSUpdateUiDuringFrameDetector);
+ mUIManagerModule.setUiManagerDebugListener(mDidJSUpdateUiDuringFrameDetector);
+ mChoreographer.postFrameCallback(this);
+ }
+
+ public void startAndRecordFpsAtEachFrame() {
+ mTimeToFps = new TreeMap();
+ mIsRecordingFpsInfoAtEachFrame = true;
+ start();
+ }
+
+ public void stop() {
+ mShouldStop = true;
+ mReactContext.getCatalystInstance().removeBridgeIdleDebugListener(
+ mDidJSUpdateUiDuringFrameDetector);
+ mUIManagerModule.setUiManagerDebugListener(null);
+ }
+
+ public double getFPS() {
+ if (mLastFrameTime == mFirstFrameTime) {
+ return 0;
+ }
+ return ((double) (getNumFrames()) * 1e9) / (mLastFrameTime - mFirstFrameTime);
+ }
+
+ public double getJSFPS() {
+ if (mLastFrameTime == mFirstFrameTime) {
+ return 0;
+ }
+ return ((double) (getNumJSFrames()) * 1e9) / (mLastFrameTime - mFirstFrameTime);
+ }
+
+ public int getNumFrames() {
+ return mNumFrameCallbacks - 1;
+ }
+
+ public int getNumJSFrames() {
+ return mNumFrameCallbacksWithBatchDispatches - 1;
+ }
+
+ public int getExpectedNumFrames() {
+ double totalTimeMS = getTotalTimeMS();
+ int expectedFrames = (int) (totalTimeMS / EXPECTED_FRAME_TIME + 1);
+ return expectedFrames;
+ }
+
+ public int getTotalTimeMS() {
+ return (int) ((double) mLastFrameTime - mFirstFrameTime) / 1000000;
+ }
+
+ /**
+ * Returns the FpsInfo as if stop had been called at the given upToTimeMs. Only valid if
+ * monitoring was started with {@link #startAndRecordFpsAtEachFrame()}.
+ */
+ public @Nullable FpsInfo getFpsInfo(long upToTimeMs) {
+ Assertions.assertNotNull(mTimeToFps, "FPS was not recorded at each frame!");
+ Map.Entry bestEntry = mTimeToFps.floorEntry(upToTimeMs);
+ if (bestEntry == null) {
+ return null;
+ }
+ return bestEntry.getValue();
+ }
+
+ public void reset() {
+ mFirstFrameTime = -1;
+ mLastFrameTime = -1;
+ mNumFrameCallbacks = 0;
+ mNumFrameCallbacksWithBatchDispatches = 0;
+ mIsRecordingFpsInfoAtEachFrame = false;
+ mTimeToFps = null;
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/debug/SourceCodeModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/debug/SourceCodeModule.java
new file mode 100644
index 00000000000000..07022a7e76c364
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/modules/debug/SourceCodeModule.java
@@ -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.modules.debug;
+
+import javax.annotation.Nullable;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import com.facebook.react.bridge.Callback;
+import com.facebook.react.bridge.BaseJavaModule;
+import com.facebook.react.bridge.ReactMethod;
+import com.facebook.react.bridge.WritableMap;
+import com.facebook.react.bridge.WritableNativeMap;
+
+/**
+ * Module that exposes the URL to the source code map (used for exception stack trace parsing) to JS
+ */
+public class SourceCodeModule extends BaseJavaModule {
+
+ private final String mSourceMapUrl;
+ private final String mSourceUrl;
+
+ public SourceCodeModule(String sourceUrl, String sourceMapUrl) {
+ mSourceMapUrl = sourceMapUrl;
+ mSourceUrl = sourceUrl;
+ }
+
+ @Override
+ public String getName() {
+ return "RKSourceCode";
+ }
+
+ @ReactMethod
+ public void getScriptText(final Callback onSuccess, final Callback onError) {
+ WritableMap map = new WritableNativeMap();
+ map.putString("fullSourceMappingURL", mSourceMapUrl);
+ onSuccess.invoke(map);
+ }
+
+ @Override
+ public @Nullable Map getConstants() {
+ HashMap constants = new HashMap();
+ constants.put("scriptURL", mSourceUrl);
+ return constants;
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/fresco/FrescoModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/fresco/FrescoModule.java
new file mode 100644
index 00000000000000..20f163fe3648ac
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/modules/fresco/FrescoModule.java
@@ -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.modules.fresco;
+
+import android.content.Context;
+
+import com.facebook.cache.common.CacheKey;
+import com.facebook.common.internal.AndroidPredicates;
+import com.facebook.common.soloader.SoLoaderShim;
+import com.facebook.drawee.backends.pipeline.Fresco;
+import com.facebook.imagepipeline.backends.okhttp.OkHttpImagePipelineConfigFactory;
+import com.facebook.imagepipeline.core.ImagePipelineConfig;
+import com.facebook.imagepipeline.core.ImagePipelineFactory;
+import com.facebook.react.bridge.ReactApplicationContext;
+import com.facebook.react.bridge.ReactContextBaseJavaModule;
+import com.facebook.react.modules.common.ModuleDataCleaner;
+import com.facebook.react.modules.network.OkHttpClientProvider;
+import com.facebook.soloader.SoLoader;
+
+import com.squareup.okhttp.OkHttpClient;
+
+/**
+ * Module to initialize the Fresco library.
+ *
+ * Does not expose any methods to JavaScript code. For initialization and cleanup only.
+ */
+public class FrescoModule extends ReactContextBaseJavaModule implements
+ ModuleDataCleaner.Cleanable {
+
+ public FrescoModule(ReactApplicationContext reactContext) {
+ super(reactContext);
+ }
+
+ @Override
+ public void initialize() {
+ super.initialize();
+ // Make sure the SoLoaderShim is configured to use our loader for native libraries.
+ // This code can be removed if using Fresco from Maven rather than from source
+ SoLoaderShim.setHandler(
+ new SoLoaderShim.Handler() {
+ @Override
+ public void loadLibrary(String libraryName) {
+ SoLoader.loadLibrary(libraryName);
+ }
+ });
+ Context context = this.getReactApplicationContext().getApplicationContext();
+ OkHttpClient okHttpClient = OkHttpClientProvider.getOkHttpClient();
+ ImagePipelineConfig config = OkHttpImagePipelineConfigFactory
+ .newBuilder(context, okHttpClient)
+ .setDownsampleEnabled(false)
+ .build();
+ Fresco.initialize(context, config);
+ }
+
+ @Override
+ public String getName() {
+ return "FrescoModule";
+ }
+
+ @Override
+ public void clearSensitiveData() {
+ // Clear image cache.
+ ImagePipelineFactory imagePipelineFactory = Fresco.getImagePipelineFactory();
+ imagePipelineFactory.getBitmapMemoryCache().removeAll(AndroidPredicates.True());
+ imagePipelineFactory.getEncodedMemoryCache().removeAll(AndroidPredicates.True());
+ imagePipelineFactory.getMainDiskStorageCache().clearAll();
+ imagePipelineFactory.getSmallImageDiskStorageCache().clearAll();
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.java
new file mode 100644
index 00000000000000..b957173cbbaecf
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.java
@@ -0,0 +1,289 @@
+/**
+ * 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.network;
+
+import javax.annotation.Nullable;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import com.facebook.react.bridge.Callback;
+import com.facebook.react.bridge.GuardedAsyncTask;
+import com.facebook.react.bridge.ReactApplicationContext;
+import com.facebook.react.bridge.ReactContextBaseJavaModule;
+import com.facebook.react.bridge.ReactMethod;
+import com.facebook.react.bridge.ReadableArray;
+import com.facebook.react.bridge.ReadableMap;
+import com.facebook.react.modules.network.OkHttpClientProvider;
+
+import com.squareup.okhttp.Headers;
+import com.squareup.okhttp.MediaType;
+import com.squareup.okhttp.MultipartBuilder;
+import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.Request;
+import com.squareup.okhttp.RequestBody;
+import com.squareup.okhttp.Response;
+
+/**
+ * Implements the XMLHttpRequest JavaScript interface.
+ */
+public final class NetworkingModule extends ReactContextBaseJavaModule {
+
+ private static final String CONTENT_ENCODING_HEADER_NAME = "content-encoding";
+ private static final String CONTENT_TYPE_HEADER_NAME = "content-type";
+ private static final String REQUEST_BODY_KEY_STRING = "string";
+ private static final String REQUEST_BODY_KEY_URI = "uri";
+ private static final String REQUEST_BODY_KEY_FORMDATA = "formData";
+ private static final String USER_AGENT_HEADER_NAME = "user-agent";
+
+ private final OkHttpClient mClient;
+ private final @Nullable String mDefaultUserAgent;
+ private boolean mShuttingDown;
+
+ /* package */ NetworkingModule(
+ ReactApplicationContext reactContext,
+ @Nullable String defaultUserAgent,
+ OkHttpClient client) {
+ super(reactContext);
+ mClient = client;
+ mShuttingDown = false;
+ mDefaultUserAgent = defaultUserAgent;
+ }
+
+ /**
+ * @param reactContext the ReactContext of the application
+ */
+ public NetworkingModule(ReactApplicationContext reactContext) {
+ this(reactContext, null, OkHttpClientProvider.getOkHttpClient());
+ }
+
+ /**
+ * @param reactContext the ReactContext of the application
+ * @param defaultUserAgent the User-Agent header that will be set for all requests where the
+ * caller does not provide one explicitly
+ */
+ public NetworkingModule(ReactApplicationContext reactContext, String defaultUserAgent) {
+ this(reactContext, defaultUserAgent, OkHttpClientProvider.getOkHttpClient());
+ }
+
+ @Override
+ public String getName() {
+ return "RCTNetworking";
+ }
+
+ @Override
+ public void onCatalystInstanceDestroy() {
+ mShuttingDown = true;
+ mClient.cancel(null);
+ }
+
+ @ReactMethod
+ public void sendRequest(
+ String method,
+ String url,
+ int requestId,
+ ReadableArray headers,
+ ReadableMap data,
+ final Callback callback) {
+ // We need to call the callback to avoid leaking memory on JS even when input for sending
+ // request is erroneous or insufficient. For non-http based failures we use code 0, which is
+ // interpreted as a transport error.
+ // Callback accepts following arguments: responseCode, headersString, responseBody
+
+ Request.Builder requestBuilder = new Request.Builder().url(url);
+
+ if (requestId != 0) {
+ requestBuilder.tag(requestId);
+ }
+
+ Headers requestHeaders = extractHeaders(headers, data);
+ if (requestHeaders == null) {
+ callback.invoke(0, null, "Unrecognized headers format");
+ return;
+ }
+ String contentType = requestHeaders.get(CONTENT_TYPE_HEADER_NAME);
+ String contentEncoding = requestHeaders.get(CONTENT_ENCODING_HEADER_NAME);
+ requestBuilder.headers(requestHeaders);
+
+ if (data == null) {
+ requestBuilder.method(method, null);
+ } else if (data.hasKey(REQUEST_BODY_KEY_STRING)) {
+ if (contentType == null) {
+ callback.invoke(0, null, "Payload is set but no content-type header specified");
+ return;
+ }
+ String body = data.getString(REQUEST_BODY_KEY_STRING);
+ MediaType contentMediaType = MediaType.parse(contentType);
+ if (RequestBodyUtil.isGzipEncoding(contentEncoding)) {
+ RequestBody requestBody = RequestBodyUtil.createGzip(contentMediaType, body);
+ if (requestBody == null) {
+ callback.invoke(0, null, "Failed to gzip request body");
+ return;
+ }
+ requestBuilder.method(method, requestBody);
+ } else {
+ requestBuilder.method(method, RequestBody.create(contentMediaType, body));
+ }
+ } else if (data.hasKey(REQUEST_BODY_KEY_URI)) {
+ if (contentType == null) {
+ callback.invoke(0, null, "Payload is set but no content-type header specified");
+ return;
+ }
+ String uri = data.getString(REQUEST_BODY_KEY_URI);
+ InputStream fileInputStream =
+ RequestBodyUtil.getFileInputStream(getReactApplicationContext(), uri);
+ if (fileInputStream == null) {
+ callback.invoke(0, null, "Could not retrieve file for uri " + uri);
+ return;
+ }
+ requestBuilder.method(
+ method,
+ RequestBodyUtil.create(MediaType.parse(contentType), fileInputStream));
+ } else if (data.hasKey(REQUEST_BODY_KEY_FORMDATA)) {
+ if (contentType == null) {
+ contentType = "multipart/form-data";
+ }
+ ReadableArray parts = data.getArray(REQUEST_BODY_KEY_FORMDATA);
+ MultipartBuilder multipartBuilder = constructMultipartBody(parts, contentType, callback);
+ if (multipartBuilder == null) {
+ return;
+ }
+ requestBuilder.method(method, multipartBuilder.build());
+ } else {
+ // Nothing in data payload, at least nothing we could understand anyway.
+ // Ignore and treat it as if it were null.
+ requestBuilder.method(method, null);
+ }
+
+ mClient.newCall(requestBuilder.build()).enqueue(
+ new com.squareup.okhttp.Callback() {
+ @Override
+ public void onFailure(Request request, IOException e) {
+ if (mShuttingDown) {
+ return;
+ }
+ // We need to call the callback to avoid leaking memory on JS even when input for
+ // sending request is erronous or insufficient. For non-http based failures we use
+ // code 0, which is interpreted as a transport error
+ callback.invoke(0, null, e.getMessage());
+ }
+
+ @Override
+ public void onResponse(Response response) throws IOException {
+ if (mShuttingDown) {
+ return;
+ }
+ // TODO(5472580) handle headers properly
+ String responseBody;
+ try {
+ responseBody = response.body().string();
+ } catch (IOException e) {
+ // The stream has been cancelled or closed, nothing we can do
+ callback.invoke(0, null, e.getMessage());
+ return;
+ }
+ callback.invoke(response.code(), null, responseBody);
+ }
+ });
+ }
+
+ @ReactMethod
+ public void abortRequest(final int requestId) {
+ // We have to use AsyncTask since this might trigger a NetworkOnMainThreadException, this is an
+ // open issue on OkHttp: https://github.com/square/okhttp/issues/869
+ new GuardedAsyncTask(getReactApplicationContext()) {
+ @Override
+ protected void doInBackgroundGuarded(Void... params) {
+ mClient.cancel(requestId);
+ }
+ }.execute();
+ }
+
+ private @Nullable MultipartBuilder constructMultipartBody(
+ ReadableArray body,
+ String contentType,
+ Callback callback) {
+ MultipartBuilder multipartBuilder = new MultipartBuilder();
+ multipartBuilder.type(MediaType.parse(contentType));
+
+ for (int i = 0, size = body.size(); i < size; i++) {
+ ReadableMap bodyPart = body.getMap(i);
+
+ // Determine part's content type.
+ ReadableArray headersArray = bodyPart.getArray("headers");
+ Headers headers = extractHeaders(headersArray, null);
+ if (headers == null) {
+ callback.invoke(0, null, "Missing or invalid header format for FormData part.");
+ return null;
+ }
+ MediaType partContentType = null;
+ String partContentTypeStr = headers.get(CONTENT_TYPE_HEADER_NAME);
+ if (partContentTypeStr != null) {
+ partContentType = MediaType.parse(partContentTypeStr);
+ // Remove the content-type header because MultipartBuilder gets it explicitly as an
+ // argument and doesn't expect it in the headers array.
+ headers = headers.newBuilder().removeAll(CONTENT_TYPE_HEADER_NAME).build();
+ }
+
+ if (bodyPart.hasKey(REQUEST_BODY_KEY_STRING)) {
+ String bodyValue = bodyPart.getString(REQUEST_BODY_KEY_STRING);
+ multipartBuilder.addPart(headers, RequestBody.create(partContentType, bodyValue));
+ } else if (bodyPart.hasKey(REQUEST_BODY_KEY_URI)) {
+ if (partContentType == null) {
+ callback.invoke(0, null, "Binary FormData part needs a content-type header.");
+ return null;
+ }
+ String fileContentUriStr = bodyPart.getString(REQUEST_BODY_KEY_URI);
+ InputStream fileInputStream =
+ RequestBodyUtil.getFileInputStream(getReactApplicationContext(), fileContentUriStr);
+ if (fileInputStream == null) {
+ callback.invoke(0, null, "Could not retrieve file for uri " + fileContentUriStr);
+ return null;
+ }
+ multipartBuilder.addPart(headers, RequestBodyUtil.create(partContentType, fileInputStream));
+ } else {
+ callback.invoke(0, null, "Unrecognized FormData part.");
+ }
+ }
+ return multipartBuilder;
+ }
+
+ /**
+ * Extracts the headers from the Array. If the format is invalid, this method will return null.
+ */
+ private @Nullable Headers extractHeaders(
+ @Nullable ReadableArray headersArray,
+ @Nullable ReadableMap requestData) {
+ if (headersArray == null) {
+ return null;
+ }
+ Headers.Builder headersBuilder = new Headers.Builder();
+ for (int headersIdx = 0, size = headersArray.size(); headersIdx < size; headersIdx++) {
+ ReadableArray header = headersArray.getArray(headersIdx);
+ if (header == null || header.size() != 2) {
+ return null;
+ }
+ String headerName = header.getString(0);
+ String headerValue = header.getString(1);
+ headersBuilder.add(headerName, headerValue);
+ }
+ if (headersBuilder.get(USER_AGENT_HEADER_NAME) == null && mDefaultUserAgent != null) {
+ headersBuilder.add(USER_AGENT_HEADER_NAME, mDefaultUserAgent);
+ }
+
+ // Sanitize content encoding header, supported only when request specify payload as string
+ boolean isGzipSupported = requestData != null && requestData.hasKey(REQUEST_BODY_KEY_STRING);
+ if (!isGzipSupported) {
+ headersBuilder.removeAll(CONTENT_ENCODING_HEADER_NAME);
+ }
+
+ return headersBuilder.build();
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/network/OkHttpClientProvider.java b/ReactAndroid/src/main/java/com/facebook/react/modules/network/OkHttpClientProvider.java
new file mode 100644
index 00000000000000..fb700201302c34
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/modules/network/OkHttpClientProvider.java
@@ -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.modules.network;
+
+import java.util.concurrent.TimeUnit;
+import com.squareup.okhttp.OkHttpClient;
+
+/**
+ * Helper class that provides the same OkHttpClient instance that will be used for all networking
+ * requests.
+ */
+public class OkHttpClientProvider {
+
+ // Centralized OkHttpClient for all networking requests.
+ private static OkHttpClient sClient;
+
+ public static OkHttpClient getOkHttpClient() {
+ if (sClient == null) {
+ // TODO: #7108751 plug in stetho
+ sClient = new OkHttpClient();
+
+ // No timeouts by default
+ sClient.setConnectTimeout(0, TimeUnit.MILLISECONDS);
+ sClient.setReadTimeout(0, TimeUnit.MILLISECONDS);
+ sClient.setWriteTimeout(0, TimeUnit.MILLISECONDS);
+ }
+ return sClient;
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/network/RequestBodyUtil.java b/ReactAndroid/src/main/java/com/facebook/react/modules/network/RequestBodyUtil.java
new file mode 100644
index 00000000000000..7ce69c37e2d99a
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/modules/network/RequestBodyUtil.java
@@ -0,0 +1,115 @@
+/**
+ * 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.network;
+
+import javax.annotation.Nullable;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.zip.GZIPOutputStream;
+
+import android.content.Context;
+import android.net.Uri;
+
+import com.facebook.common.logging.FLog;
+import com.facebook.react.common.ReactConstants;
+
+import com.squareup.okhttp.MediaType;
+import com.squareup.okhttp.RequestBody;
+import com.squareup.okhttp.internal.Util;
+import okio.BufferedSink;
+import okio.Okio;
+import okio.Source;
+
+/**
+ * Helper class that provides the necessary methods for creating the RequestBody from a file
+ * specification, such as a contentUri.
+ */
+/*package*/ class RequestBodyUtil {
+
+ private static final String CONTENT_ENCODING_GZIP = "gzip";
+
+ /**
+ * Returns whether encode type indicates the body needs to be gzip-ed.
+ */
+ public static boolean isGzipEncoding(@Nullable final String encodingType) {
+ return CONTENT_ENCODING_GZIP.equalsIgnoreCase(encodingType);
+ }
+
+ /**
+ * Returns the input stream for a file given by its contentUri. Returns null if the file has not
+ * been found or if an error as occurred.
+ */
+ public static @Nullable InputStream getFileInputStream(
+ Context context,
+ String fileContentUriStr) {
+ try {
+ Uri fileContentUri = Uri.parse(fileContentUriStr);
+ return context.getContentResolver().openInputStream(fileContentUri);
+ } catch (Exception e) {
+ FLog.e(
+ ReactConstants.TAG,
+ "Could not retrieve file for contentUri " + fileContentUriStr,
+ e);
+ return null;
+ }
+ }
+
+ /**
+ * Creates a RequestBody from a mediaType and gzip-ed body string
+ */
+ public static @Nullable RequestBody createGzip(
+ final MediaType mediaType,
+ final String body) {
+ ByteArrayOutputStream gzipByteArrayOutputStream = new ByteArrayOutputStream();
+ try {
+ OutputStream gzipOutputStream = new GZIPOutputStream(gzipByteArrayOutputStream);
+ gzipOutputStream.write(body.getBytes());
+ gzipOutputStream.close();
+ } catch (IOException e) {
+ return null;
+ }
+ return RequestBody.create(mediaType, gzipByteArrayOutputStream.toByteArray());
+ }
+
+ /**
+ * Creates a RequestBody from a mediaType and inputStream given.
+ */
+ public static RequestBody create(final MediaType mediaType, final InputStream inputStream) {
+ return new RequestBody() {
+ @Override
+ public MediaType contentType() {
+ return mediaType;
+ }
+
+ @Override
+ public long contentLength() {
+ try {
+ return inputStream.available();
+ } catch (IOException e) {
+ return 0;
+ }
+ }
+
+ @Override
+ public void writeTo(BufferedSink sink) throws IOException {
+ Source source = null;
+ try {
+ source = Okio.source(inputStream);
+ sink.writeAll(source);
+ } finally {
+ Util.closeQuietly(source);
+ }
+ }
+ };
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/storage/AsyncLocalStorageUtil.java b/ReactAndroid/src/main/java/com/facebook/react/modules/storage/AsyncLocalStorageUtil.java
new file mode 100644
index 00000000000000..36340f0aa5903f
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/modules/storage/AsyncLocalStorageUtil.java
@@ -0,0 +1,147 @@
+/**
+ * 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.storage;
+
+import javax.annotation.Nullable;
+
+import java.util.Arrays;
+import java.util.Iterator;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.text.TextUtils;
+
+import com.facebook.react.bridge.ReadableArray;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import static com.facebook.react.modules.storage.CatalystSQLiteOpenHelper.KEY_COLUMN;
+import static com.facebook.react.modules.storage.CatalystSQLiteOpenHelper.TABLE_CATALYST;
+import static com.facebook.react.modules.storage.CatalystSQLiteOpenHelper.VALUE_COLUMN;
+
+/**
+ * Helper for database operations.
+ */
+/* package */ class AsyncLocalStorageUtil {
+
+ /**
+ * Build the String required for an SQL select statement:
+ * WHERE key IN (?, ?, ..., ?)
+ * without 'WHERE' and with selectionCount '?'
+ */
+ /* package */ static String buildKeySelection(int selectionCount) {
+ String[] list = new String[selectionCount];
+ Arrays.fill(list, "?");
+ return KEY_COLUMN + " IN (" + TextUtils.join(", ", list) + ")";
+ }
+
+ /**
+ * Build the String[] arguments needed for an SQL selection, i.e.:
+ * {a, b, c}
+ * to be used in the SQL select statement: WHERE key in (?, ?, ?)
+ */
+ /* package */ static String[] buildKeySelectionArgs(ReadableArray keys) {
+ String[] selectionArgs = new String[keys.size()];
+ for (int keyIndex = 0; keyIndex < keys.size(); keyIndex++) {
+ selectionArgs[keyIndex] = keys.getString(keyIndex);
+ }
+ return selectionArgs;
+ }
+
+ /**
+ * Returns the value of the given key, or null if not found.
+ */
+ /* package */ static @Nullable String getItemImpl(SQLiteDatabase db, String key) {
+ String[] columns = {VALUE_COLUMN};
+ String[] selectionArgs = {key};
+
+ Cursor cursor = db.query(
+ TABLE_CATALYST,
+ columns,
+ KEY_COLUMN + "=?",
+ selectionArgs,
+ null,
+ null,
+ null);
+
+ try {
+ if (!cursor.moveToFirst()) {
+ return null;
+ } else {
+ return cursor.getString(0);
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+
+ /**
+ * Sets the value for the key given, returns true if successful, false otherwise.
+ */
+ /* package */ static boolean setItemImpl(SQLiteDatabase db, String key, String value) {
+ ContentValues contentValues = new ContentValues();
+ contentValues.put(KEY_COLUMN, key);
+ contentValues.put(VALUE_COLUMN, value);
+
+ long inserted = db.insertWithOnConflict(
+ TABLE_CATALYST,
+ null,
+ contentValues,
+ SQLiteDatabase.CONFLICT_REPLACE);
+
+ return (-1 != inserted);
+ }
+
+ /**
+ * Does the actual merge of the (key, value) pair with the value stored in the database.
+ * NB: This assumes that a database lock is already in effect!
+ * @return the errorCode of the operation
+ */
+ /* package */ static boolean mergeImpl(SQLiteDatabase db, String key, String value)
+ throws JSONException {
+ String oldValue = getItemImpl(db, key);
+ String newValue;
+
+ if (oldValue == null) {
+ newValue = value;
+ } else {
+ JSONObject oldJSON = new JSONObject(oldValue);
+ JSONObject newJSON = new JSONObject(value);
+ deepMergeInto(oldJSON, newJSON);
+ newValue = oldJSON.toString();
+ }
+
+ return setItemImpl(db, key, newValue);
+ }
+
+ /**
+ * Merges two {@link JSONObject}s. The newJSON object will be merged with the oldJSON object by
+ * either overriding its values, or merging them (if the values of the same key in both objects
+ * are of type {@link JSONObject}). oldJSON will contain the result of this merge.
+ */
+ private static void deepMergeInto(JSONObject oldJSON, JSONObject newJSON)
+ throws JSONException {
+ Iterator> keys = newJSON.keys();
+ while (keys.hasNext()) {
+ String key = (String) keys.next();
+
+ JSONObject newJSONObject = newJSON.optJSONObject(key);
+ JSONObject oldJSONObject = oldJSON.optJSONObject(key);
+ if (newJSONObject != null && oldJSONObject != null) {
+ deepMergeInto(oldJSONObject, newJSONObject);
+ oldJSON.put(key, oldJSONObject);
+ } else {
+ oldJSON.put(key, newJSON.get(key));
+ }
+ }
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/storage/AsyncStorageErrorUtil.java b/ReactAndroid/src/main/java/com/facebook/react/modules/storage/AsyncStorageErrorUtil.java
new file mode 100644
index 00000000000000..75f25617e5071b
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/modules/storage/AsyncStorageErrorUtil.java
@@ -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.modules.storage;
+
+import javax.annotation.Nullable;
+
+import com.facebook.react.bridge.Arguments;
+import com.facebook.react.bridge.WritableMap;
+
+/**
+ * Helper class for database errors.
+ */
+public class AsyncStorageErrorUtil {
+
+ /**
+ * Create Error object to be passed back to the JS callback.
+ */
+ /* package */ static WritableMap getError(@Nullable String key, String errorMessage) {
+ WritableMap errorMap = Arguments.createMap();
+ errorMap.putString("message", errorMessage);
+ if (key != null) {
+ errorMap.putString("key", key);
+ }
+ return errorMap;
+ }
+
+ /* package */ static WritableMap getInvalidKeyError(@Nullable String key) {
+ return getError(key, "Invalid key");
+ }
+
+ /* package */ static WritableMap getInvalidValueError(@Nullable String key) {
+ return getError(key, "Invalid Value");
+ }
+
+ /* package */ static WritableMap getDBError(@Nullable String key) {
+ return getError(key, "Database Error");
+ }
+
+
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/storage/AsyncStorageModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/storage/AsyncStorageModule.java
new file mode 100644
index 00000000000000..601528a0fbabf5
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/modules/storage/AsyncStorageModule.java
@@ -0,0 +1,369 @@
+/**
+ * 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.storage;
+
+import javax.annotation.Nullable;
+
+import java.util.HashSet;
+
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteStatement;
+
+import com.facebook.common.logging.FLog;
+import com.facebook.infer.annotation.Assertions;
+import com.facebook.react.bridge.Arguments;
+import com.facebook.react.bridge.Callback;
+import com.facebook.react.bridge.GuardedAsyncTask;
+import com.facebook.react.bridge.ReactApplicationContext;
+import com.facebook.react.bridge.ReactContextBaseJavaModule;
+import com.facebook.react.bridge.ReactMethod;
+import com.facebook.react.bridge.ReadableArray;
+import com.facebook.react.bridge.WritableArray;
+import com.facebook.react.common.ReactConstants;
+import com.facebook.react.common.SetBuilder;
+import com.facebook.react.modules.common.ModuleDataCleaner;
+
+import static com.facebook.react.modules.storage.CatalystSQLiteOpenHelper.KEY_COLUMN;
+import static com.facebook.react.modules.storage.CatalystSQLiteOpenHelper.TABLE_CATALYST;
+import static com.facebook.react.modules.storage.CatalystSQLiteOpenHelper.VALUE_COLUMN;
+
+public final class AsyncStorageModule
+ extends ReactContextBaseJavaModule implements ModuleDataCleaner.Cleanable {
+
+ private @Nullable SQLiteDatabase mDb;
+ private boolean mShuttingDown = false;
+
+ public AsyncStorageModule(ReactApplicationContext reactContext) {
+ super(reactContext);
+ }
+
+ @Override
+ public String getName() {
+ return "AsyncSQLiteDBStorage";
+ }
+
+ @Override
+ public void initialize() {
+ super.initialize();
+ mShuttingDown = false;
+ }
+
+ @Override
+ public void onCatalystInstanceDestroy() {
+ mShuttingDown = true;
+ if (mDb != null && mDb.isOpen()) {
+ mDb.close();
+ mDb = null;
+ }
+ }
+
+ @Override
+ public void clearSensitiveData() {
+ // Clear local storage. If fails, crash, since the app is potentially in a bad state and could
+ // cause a privacy violation. We're still not recovering from this well, but at least the error
+ // will be reported to the server.
+ clear(
+ new Callback() {
+ @Override
+ public void invoke(Object... args) {
+ if (args.length > 0) {
+ throw new RuntimeException("Clearing AsyncLocalStorage failed: " + args[0]);
+ }
+ FLog.d(ReactConstants.TAG, "Cleaned AsyncLocalStorage.");
+ }
+ });
+ }
+
+ /**
+ * Given an array of keys, this returns a map of (key, value) pairs for the keys found, and
+ * (key, null) for the keys that haven't been found.
+ */
+ @ReactMethod
+ public void multiGet(final ReadableArray keys, final Callback callback) {
+ if (keys == null) {
+ callback.invoke(AsyncStorageErrorUtil.getInvalidKeyError(null), null);
+ return;
+ }
+
+ new GuardedAsyncTask(getReactApplicationContext()) {
+ @Override
+ protected void doInBackgroundGuarded(Void... params) {
+ if (!ensureDatabase()) {
+ callback.invoke(AsyncStorageErrorUtil.getDBError(null), null);
+ return;
+ }
+
+ String[] columns = {KEY_COLUMN, VALUE_COLUMN};
+ HashSet keysRemaining = SetBuilder.newHashSet();
+ WritableArray data = Arguments.createArray();
+ Cursor cursor = Assertions.assertNotNull(mDb).query(
+ TABLE_CATALYST,
+ columns,
+ AsyncLocalStorageUtil.buildKeySelection(keys.size()),
+ AsyncLocalStorageUtil.buildKeySelectionArgs(keys),
+ null,
+ null,
+ null);
+
+ try {
+ if (cursor.getCount() != keys.size()) {
+ // some keys have not been found - insert them with null into the final array
+ for (int keyIndex = 0; keyIndex < keys.size(); keyIndex++) {
+ keysRemaining.add(keys.getString(keyIndex));
+ }
+ }
+
+ if (cursor.moveToFirst()) {
+ do {
+ WritableArray row = Arguments.createArray();
+ row.pushString(cursor.getString(0));
+ row.pushString(cursor.getString(1));
+ data.pushArray(row);
+ keysRemaining.remove(cursor.getString(0));
+ } while (cursor.moveToNext());
+
+ }
+ } catch (Exception e) {
+ FLog.w(ReactConstants.TAG, "Exception in database multiGet ", e);
+ callback.invoke(AsyncStorageErrorUtil.getError(null, e.getMessage()), null);
+ } finally {
+ cursor.close();
+ }
+
+ for (String key : keysRemaining) {
+ WritableArray row = Arguments.createArray();
+ row.pushString(key);
+ row.pushNull();
+ data.pushArray(row);
+ }
+ keysRemaining.clear();
+ callback.invoke(null, data);
+ }
+ }.execute();
+ }
+
+ /**
+ * Inserts multiple (key, value) pairs. If one or more of the pairs cannot be inserted, this will
+ * return AsyncLocalStorageFailure, but all other pairs will have been inserted.
+ * The insertion will replace conflicting (key, value) pairs.
+ */
+ @ReactMethod
+ public void multiSet(final ReadableArray keyValueArray, final Callback callback) {
+ if (keyValueArray.size() == 0) {
+ callback.invoke(AsyncStorageErrorUtil.getInvalidKeyError(null));
+ return;
+ }
+
+ new GuardedAsyncTask(getReactApplicationContext()) {
+ @Override
+ protected void doInBackgroundGuarded(Void... params) {
+ if (!ensureDatabase()) {
+ callback.invoke(AsyncStorageErrorUtil.getDBError(null));
+ return;
+ }
+
+ String sql = "INSERT OR REPLACE INTO " + TABLE_CATALYST + " VALUES (?, ?);";
+ SQLiteStatement statement = Assertions.assertNotNull(mDb).compileStatement(sql);
+ mDb.beginTransaction();
+ try {
+ for (int idx=0; idx < keyValueArray.size(); idx++) {
+ if (keyValueArray.getArray(idx).size() != 2) {
+ callback.invoke(AsyncStorageErrorUtil.getInvalidValueError(null));
+ return;
+ }
+ if (keyValueArray.getArray(idx).getString(0) == null) {
+ callback.invoke(AsyncStorageErrorUtil.getInvalidKeyError(null));
+ return;
+ }
+ if (keyValueArray.getArray(idx).getString(1) == null) {
+ callback.invoke(AsyncStorageErrorUtil.getInvalidValueError(null));
+ return;
+ }
+
+ statement.clearBindings();
+ statement.bindString(1, keyValueArray.getArray(idx).getString(0));
+ statement.bindString(2, keyValueArray.getArray(idx).getString(1));
+ statement.execute();
+ }
+ mDb.setTransactionSuccessful();
+ } catch (Exception e) {
+ FLog.w(ReactConstants.TAG, "Exception in database multiSet ", e);
+ callback.invoke(AsyncStorageErrorUtil.getError(null, e.getMessage()));
+ } finally {
+ mDb.endTransaction();
+ }
+ callback.invoke();
+ }
+ }.execute();
+ }
+
+ /**
+ * Removes all rows of the keys given.
+ */
+ @ReactMethod
+ public void multiRemove(final ReadableArray keys, final Callback callback) {
+ if (keys.size() == 0) {
+ callback.invoke(AsyncStorageErrorUtil.getInvalidKeyError(null));
+ return;
+ }
+
+ new GuardedAsyncTask(getReactApplicationContext()) {
+ @Override
+ protected void doInBackgroundGuarded(Void... params) {
+ if (!ensureDatabase()) {
+ callback.invoke(AsyncStorageErrorUtil.getDBError(null));
+ return;
+ }
+
+ try {
+ Assertions.assertNotNull(mDb).delete(
+ TABLE_CATALYST,
+ AsyncLocalStorageUtil.buildKeySelection(keys.size()),
+ AsyncLocalStorageUtil.buildKeySelectionArgs(keys));
+ } catch (Exception e) {
+ FLog.w(ReactConstants.TAG, "Exception in database multiRemove ", e);
+ callback.invoke(AsyncStorageErrorUtil.getError(null, e.getMessage()));
+ }
+ callback.invoke();
+ }
+ }.execute();
+ }
+
+ /**
+ * Given an array of (key, value) pairs, this will merge the given values with the stored values
+ * of the given keys, if they exist.
+ */
+ @ReactMethod
+ public void multiMerge(final ReadableArray keyValueArray, final Callback callback) {
+ new GuardedAsyncTask(getReactApplicationContext()) {
+ @Override
+ protected void doInBackgroundGuarded(Void... params) {
+ if (!ensureDatabase()) {
+ callback.invoke(AsyncStorageErrorUtil.getDBError(null));
+ return;
+ }
+ Assertions.assertNotNull(mDb).beginTransaction();
+ try {
+ for (int idx = 0; idx < keyValueArray.size(); idx++) {
+ if (keyValueArray.getArray(idx).size() != 2) {
+ callback.invoke(AsyncStorageErrorUtil.getInvalidValueError(null));
+ return;
+ }
+
+ if (keyValueArray.getArray(idx).getString(0) == null) {
+ callback.invoke(AsyncStorageErrorUtil.getInvalidKeyError(null));
+ return;
+ }
+
+ if (keyValueArray.getArray(idx).getString(1) == null) {
+ callback.invoke(AsyncStorageErrorUtil.getInvalidValueError(null));
+ return;
+ }
+
+ if (!AsyncLocalStorageUtil.mergeImpl(
+ mDb,
+ keyValueArray.getArray(idx).getString(0),
+ keyValueArray.getArray(idx).getString(1))) {
+ callback.invoke(AsyncStorageErrorUtil.getDBError(null));
+ return;
+ }
+ }
+ mDb.setTransactionSuccessful();
+ } catch (Exception e) {
+ FLog.w(ReactConstants.TAG, e.getMessage(), e);
+ callback.invoke(AsyncStorageErrorUtil.getError(null, e.getMessage()));
+ } finally {
+ mDb.endTransaction();
+ }
+ callback.invoke();
+ }
+ }.execute();
+ }
+
+ /**
+ * Clears the database.
+ */
+ @ReactMethod
+ public void clear(final Callback callback) {
+ new GuardedAsyncTask(getReactApplicationContext()) {
+ @Override
+ protected void doInBackgroundGuarded(Void... params) {
+ if (!ensureDatabase()) {
+ callback.invoke(AsyncStorageErrorUtil.getDBError(null));
+ return;
+ }
+ try {
+ Assertions.assertNotNull(mDb).delete(TABLE_CATALYST, null, null);
+ } catch (Exception e) {
+ FLog.w(ReactConstants.TAG, "Exception in database clear ", e);
+ callback.invoke(AsyncStorageErrorUtil.getError(null, e.getMessage()));
+ }
+ callback.invoke();
+ }
+ }.execute();
+ }
+
+ /**
+ * Returns an array with all keys from the database.
+ */
+ @ReactMethod
+ public void getAllKeys(final Callback callback) {
+ new GuardedAsyncTask(getReactApplicationContext()) {
+ @Override
+ protected void doInBackgroundGuarded(Void... params) {
+ if (!ensureDatabase()) {
+ callback.invoke(AsyncStorageErrorUtil.getDBError(null), null);
+ return;
+ }
+ WritableArray data = Arguments.createArray();
+ String[] columns = {KEY_COLUMN};
+ Cursor cursor = Assertions.assertNotNull(mDb)
+ .query(TABLE_CATALYST, columns, null, null, null, null, null);
+ try {
+ if (cursor.moveToFirst()) {
+ do {
+ data.pushString(cursor.getString(0));
+ } while (cursor.moveToNext());
+ }
+ } catch (Exception e) {
+ FLog.w(ReactConstants.TAG, "Exception in database getAllKeys ", e);
+ callback.invoke(AsyncStorageErrorUtil.getError(null, e.getMessage()), null);
+ } finally {
+ cursor.close();
+ }
+ callback.invoke(null, data);
+ }
+ }.execute();
+ }
+
+ /**
+ * Verify the database exists and is open.
+ */
+ private boolean ensureDatabase() {
+ if (mShuttingDown) {
+ return false;
+ }
+ if (mDb != null && mDb.isOpen()) {
+ return true;
+ }
+ mDb = initializeDatabase();
+ return true;
+ }
+
+ /**
+ * Create and/or open the database.
+ */
+ private SQLiteDatabase initializeDatabase() {
+ CatalystSQLiteOpenHelper helperForDb =
+ new CatalystSQLiteOpenHelper(getReactApplicationContext());
+ return helperForDb.getWritableDatabase();
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/storage/CatalystSQLiteOpenHelper.java b/ReactAndroid/src/main/java/com/facebook/react/modules/storage/CatalystSQLiteOpenHelper.java
new file mode 100644
index 00000000000000..facf52e153f97a
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/modules/storage/CatalystSQLiteOpenHelper.java
@@ -0,0 +1,53 @@
+/**
+ * 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.storage;
+
+import android.content.Context;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+
+// VisibleForTesting
+public class CatalystSQLiteOpenHelper extends SQLiteOpenHelper {
+
+ // VisibleForTesting
+ public static final String DATABASE_NAME = "RKStorage";
+ static final int DATABASE_VERSION = 1;
+
+ static final String TABLE_CATALYST = "catalystLocalStorage";
+ static final String KEY_COLUMN = "key";
+ static final String VALUE_COLUMN = "value";
+
+ static final String VERSION_TABLE_CREATE =
+ "CREATE TABLE " + TABLE_CATALYST + " (" +
+ KEY_COLUMN + " TEXT PRIMARY KEY, " +
+ VALUE_COLUMN + " TEXT NOT NULL" +
+ ")";
+
+ private Context mContext;
+
+ public CatalystSQLiteOpenHelper(Context context) {
+ super(context, DATABASE_NAME, null, DATABASE_VERSION);
+ mContext = context;
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ db.execSQL(VERSION_TABLE_CREATE);
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ // TODO: t5494781 implement data migration
+ if (oldVersion != newVersion) {
+ mContext.deleteDatabase(DATABASE_NAME);
+ onCreate(db);
+ }
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/AndroidInfoModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/AndroidInfoModule.java
new file mode 100644
index 00000000000000..d07eb4a5fa70e9
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/AndroidInfoModule.java
@@ -0,0 +1,37 @@
+/**
+ * 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.systeminfo;
+
+import javax.annotation.Nullable;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import android.os.Build;
+
+import com.facebook.react.bridge.BaseJavaModule;
+
+/**
+ * Module that exposes Android Constants to JS.
+ */
+public class AndroidInfoModule extends BaseJavaModule {
+
+ @Override
+ public String getName() {
+ return "AndroidConstants";
+ }
+
+ @Override
+ public @Nullable Map getConstants() {
+ HashMap constants = new HashMap();
+ constants.put("Version", Build.VERSION.SDK_INT);
+ return constants;
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/toast/ToastModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/toast/ToastModule.java
new file mode 100644
index 00000000000000..d401bfa1d0b3e2
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/modules/toast/ToastModule.java
@@ -0,0 +1,52 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+package com.facebook.react.modules.toast;
+
+import android.widget.Toast;
+
+import com.facebook.react.bridge.NativeModule;
+import com.facebook.react.bridge.ReactApplicationContext;
+import com.facebook.react.bridge.ReactContext;
+import com.facebook.react.bridge.ReactContextBaseJavaModule;
+import com.facebook.react.bridge.ReactMethod;
+import com.facebook.react.common.MapBuilder;
+
+import java.util.Map;
+
+/**
+ * {@link NativeModule} that allows JS to show an Android Toast.
+ */
+public class ToastModule extends ReactContextBaseJavaModule {
+
+ private static final String DURATION_SHORT_KEY = "SHORT";
+ private static final String DURATION_LONG_KEY = "LONG";
+
+ public ToastModule(ReactApplicationContext reactContext) {
+ super(reactContext);
+ }
+
+ @Override
+ public String getName() {
+ return "ToastAndroid";
+ }
+
+ @Override
+ public Map getConstants() {
+ final Map constants = MapBuilder.newHashMap();
+ constants.put(DURATION_SHORT_KEY, Toast.LENGTH_SHORT);
+ constants.put(DURATION_LONG_KEY, Toast.LENGTH_LONG);
+ return constants;
+ }
+
+ @ReactMethod
+ public void show(String message, int duration) {
+ Toast.makeText(getReactApplicationContext(), message, duration).show();
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java b/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java
new file mode 100644
index 00000000000000..65b7b38bcb530e
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java
@@ -0,0 +1,73 @@
+/**
+ * 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.shell;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import com.facebook.react.ReactPackage;
+import com.facebook.react.bridge.JavaScriptModule;
+import com.facebook.react.bridge.NativeModule;
+import com.facebook.react.bridge.ReactApplicationContext;
+import com.facebook.react.modules.fresco.FrescoModule;
+import com.facebook.react.modules.network.NetworkingModule;
+import com.facebook.react.modules.storage.AsyncStorageModule;
+import com.facebook.react.modules.toast.ToastModule;
+import com.facebook.react.uimanager.ViewManager;
+import com.facebook.react.views.drawer.ReactDrawerLayoutManager;
+import com.facebook.react.views.image.ReactImageManager;
+import com.facebook.react.views.progressbar.ReactProgressBarViewManager;
+import com.facebook.react.views.scroll.ReactHorizontalScrollViewManager;
+import com.facebook.react.views.scroll.ReactScrollViewManager;
+import com.facebook.react.views.switchviewview.ReactSwitchManager;
+import com.facebook.react.views.text.ReactRawTextManager;
+import com.facebook.react.views.text.ReactTextViewManager;
+import com.facebook.react.views.text.ReactVirtualTextViewManager;
+import com.facebook.react.views.textinput.ReactTextInputManager;
+import com.facebook.react.views.toolbar.ReactToolbarManager;
+import com.facebook.react.views.view.ReactViewManager;
+
+/**
+ * Package defining basic modules and view managers.
+ */
+public class MainReactPackage implements ReactPackage {
+
+ @Override
+ public List createNativeModules(ReactApplicationContext reactContext) {
+ return Arrays.asList(
+ new AsyncStorageModule(reactContext),
+ new FrescoModule(reactContext),
+ new NetworkingModule(reactContext),
+ new ToastModule(reactContext));
+ }
+
+ @Override
+ public List> createJSModules() {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public List createViewManagers(ReactApplicationContext reactContext) {
+ return Arrays.asList(
+ new ReactDrawerLayoutManager(),
+ new ReactHorizontalScrollViewManager(),
+ new ReactImageManager(),
+ new ReactProgressBarViewManager(),
+ new ReactRawTextManager(),
+ new ReactScrollViewManager(),
+ new ReactSwitchManager(),
+ new ReactTextInputManager(),
+ new ReactTextViewManager(),
+ new ReactToolbarManager(),
+ new ReactViewManager(),
+ new ReactVirtualTextViewManager());
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/touch/CatalystInterceptingViewGroup.java b/ReactAndroid/src/main/java/com/facebook/react/touch/CatalystInterceptingViewGroup.java
new file mode 100644
index 00000000000000..6bc10f034a3bc3
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/touch/CatalystInterceptingViewGroup.java
@@ -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.touch;
+
+
+/**
+ * This interface should be implemented by all {@link ViewGroup} subviews that can be instantiating
+ * by {@link NativeViewHierarchyManager}. It is used to configure onInterceptTouch event listener
+ * which then is used to control touch event flow in cases in which they requested to be intercepted
+ * by some parent view based on a JS gesture detector.
+ */
+public interface CatalystInterceptingViewGroup {
+
+ /**
+ * A {@link ViewGroup} instance that implement this interface is responsible for storing the
+ * listener passed as an argument and then calling
+ * {@link OnInterceptTouchEventListener#onInterceptTouchEvent} from
+ * {@link ViewGroup#onInterceptTouchEvent} and returning the result. If some custom handling of
+ * this method apply for the view, it should be called after the listener returns and only in
+ * a case when it returns false.
+ *
+ * @param listener A callback that {@link ViewGroup} should delegate calls for
+ * {@link ViewGroup#onInterceptTouchEvent} to
+ */
+ public void setOnInterceptTouchEventListener(OnInterceptTouchEventListener listener);
+
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/touch/JSResponderHandler.java b/ReactAndroid/src/main/java/com/facebook/react/touch/JSResponderHandler.java
new file mode 100644
index 00000000000000..2e8ba61f22926e
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/touch/JSResponderHandler.java
@@ -0,0 +1,77 @@
+/**
+ * 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.touch;
+
+import javax.annotation.Nullable;
+
+import android.view.MotionEvent;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+
+/**
+ * This class coordinates JSResponder commands for {@link UIManagerModule}. It should be set as
+ * OnInterceptTouchEventListener for all newly created native views that implements
+ * {@link CatalystInterceptingViewGroup} and thanks to the information whether JSResponder is set
+ * and to which view it will correctly coordinate the return values of
+ * {@link OnInterceptTouchEventListener} such that touch events will be dispatched to the view
+ * selected by JS gesture recognizer.
+ *
+ * Single {@link CatalystInstance} should reuse same instance of this class.
+ */
+public class JSResponderHandler implements OnInterceptTouchEventListener {
+
+ private static final int JS_RESPONDER_UNSET = -1;
+
+ private volatile int mCurrentJSResponder = JS_RESPONDER_UNSET;
+ // We're holding on to the ViewParent that blocked native responders so that we can clear it
+ // when we change or clear the current JS responder.
+ private @Nullable ViewParent mViewParentBlockingNativeResponder;
+
+ public void setJSResponder(int tag, @Nullable ViewParent viewParentBlockingNativeResponder) {
+ mCurrentJSResponder = tag;
+ // We need to unblock the native responder first, otherwise we can get in a bad state: a
+ // ViewParent sets requestDisallowInterceptTouchEvent to true, which sets this setting to true
+ // to all of its ancestors. Now, if one of its ancestors sets requestDisallowInterceptTouchEvent
+ // to false, it unsets the setting for itself and all of its ancestors, which means that they
+ // can intercept events again.
+ maybeUnblockNativeResponder();
+ if (viewParentBlockingNativeResponder != null) {
+ viewParentBlockingNativeResponder.requestDisallowInterceptTouchEvent(true);
+ mViewParentBlockingNativeResponder = viewParentBlockingNativeResponder;
+ }
+ }
+
+ public void clearJSResponder() {
+ mCurrentJSResponder = JS_RESPONDER_UNSET;
+ maybeUnblockNativeResponder();
+ }
+
+ private void maybeUnblockNativeResponder() {
+ if (mViewParentBlockingNativeResponder != null) {
+ mViewParentBlockingNativeResponder.requestDisallowInterceptTouchEvent(false);
+ mViewParentBlockingNativeResponder = null;
+ }
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(ViewGroup v, MotionEvent event) {
+ int currentJSResponder = mCurrentJSResponder;
+ if (currentJSResponder != JS_RESPONDER_UNSET && event.getAction() != MotionEvent.ACTION_UP) {
+ // Don't intercept ACTION_UP events. If we return true here than UP event will not be
+ // delivered. That is because intercepted touch events are converted into CANCEL events
+ // and make all further events to be delivered to the view that intercepted the event.
+ // Therefore since "UP" event is the last event in a gesture, we should just let it reach the
+ // original target that is a child view of {@param v}.
+ // http://developer.android.com/reference/android/view/ViewGroup.html#onInterceptTouchEvent(android.view.MotionEvent)
+ return v.getId() == currentJSResponder;
+ }
+ return false;
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/touch/OnInterceptTouchEventListener.java b/ReactAndroid/src/main/java/com/facebook/react/touch/OnInterceptTouchEventListener.java
new file mode 100644
index 00000000000000..299d2f4ae0ca5b
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/touch/OnInterceptTouchEventListener.java
@@ -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.touch;
+
+import android.view.MotionEvent;
+import android.view.ViewGroup;
+
+/**
+ * Interface definition for a callback to be invoked when a onInterceptTouch is called on a
+ * {@link ViewGroup}.
+ */
+public interface OnInterceptTouchEventListener {
+
+ /**
+ * Called when a onInterceptTouch is invoked on a view group
+ * @param v The view group the onInterceptTouch has been called on
+ * @param event The motion event being dispatched down the hierarchy.
+ * @return Return true to steal motion event from the children and have the dispatched to this
+ * view, or return false to allow motion event to be delivered to children view
+ */
+ public boolean onInterceptTouchEvent(ViewGroup v, MotionEvent event);
+
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/AccessibilityHelper.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/AccessibilityHelper.java
new file mode 100644
index 00000000000000..036badada735ca
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/AccessibilityHelper.java
@@ -0,0 +1,103 @@
+/**
+ * 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.uimanager;
+
+import android.view.View;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.widget.Button;
+import android.widget.RadioButton;
+
+/**
+ * Helper class containing logic for setting accessibility View properties.
+ */
+/* package */ class AccessibilityHelper {
+
+ private static final String BUTTON = "button";
+ private static final String RADIOBUTTON_CHECKED = "radiobutton_checked";
+ private static final String RADIOBUTTON_UNCHECKED = "radiobutton_unchecked";
+
+ private static final View.AccessibilityDelegate BUTTON_DELEGATE =
+ new View.AccessibilityDelegate() {
+ @Override
+ public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(host, event);
+ event.setClassName(Button.class.getName());
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(host, info);
+ info.setClassName(Button.class.getName());
+ }
+ };
+
+ private static final View.AccessibilityDelegate RADIOBUTTON_CHECKED_DELEGATE =
+ new View.AccessibilityDelegate() {
+ @Override
+ public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(host, event);
+ event.setClassName(RadioButton.class.getName());
+ event.setChecked(true);
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(host, info);
+ info.setClassName(RadioButton.class.getName());
+ info.setCheckable(true);
+ info.setChecked(true);
+ }
+ };
+
+ private static final View.AccessibilityDelegate RADIOBUTTON_UNCHECKED_DELEGATE =
+ new View.AccessibilityDelegate() {
+ @Override
+ public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(host, event);
+ event.setClassName(RadioButton.class.getName());
+ event.setChecked(false);
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(host, info);
+ info.setClassName(RadioButton.class.getName());
+ info.setCheckable(true);
+ info.setChecked(false);
+ }
+ };
+
+ public static void updateAccessibilityComponentType(View view, String componentType) {
+ if (componentType == null) {
+ view.setAccessibilityDelegate(null);
+ return;
+ }
+ switch (componentType) {
+ case BUTTON:
+ view.setAccessibilityDelegate(BUTTON_DELEGATE);
+ break;
+ case RADIOBUTTON_CHECKED:
+ view.setAccessibilityDelegate(RADIOBUTTON_CHECKED_DELEGATE);
+ break;
+ case RADIOBUTTON_UNCHECKED:
+ view.setAccessibilityDelegate(RADIOBUTTON_UNCHECKED_DELEGATE);
+ break;
+ default:
+ view.setAccessibilityDelegate(null);
+ break;
+ }
+ }
+
+ public static void sendAccessibilityEvent(View view, int eventType) {
+ view.sendAccessibilityEvent(eventType);
+ }
+
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/AndroidManifest.xml b/ReactAndroid/src/main/java/com/facebook/react/uimanager/AndroidManifest.xml
new file mode 100644
index 00000000000000..1b7a37bdb48bbf
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/AndroidManifest.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/AppRegistry.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/AppRegistry.java
new file mode 100644
index 00000000000000..dcf4457ebe856f
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/AppRegistry.java
@@ -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.uimanager;
+
+import com.facebook.react.bridge.JavaScriptModule;
+import com.facebook.react.bridge.WritableMap;
+
+/**
+ * JS module interface - main entry point for launching react application for a given key.
+ */
+public interface AppRegistry extends JavaScriptModule {
+ void runApplication(String appKey, WritableMap appParameters);
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseCSSPropertyApplicator.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseCSSPropertyApplicator.java
new file mode 100644
index 00000000000000..810abd520d862c
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseCSSPropertyApplicator.java
@@ -0,0 +1,146 @@
+/**
+ * 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.uimanager;
+
+import java.util.Locale;
+
+import com.facebook.csslayout.CSSAlign;
+import com.facebook.csslayout.CSSConstants;
+import com.facebook.csslayout.CSSFlexDirection;
+import com.facebook.csslayout.CSSJustify;
+import com.facebook.csslayout.CSSNode;
+import com.facebook.csslayout.CSSPositionType;
+import com.facebook.csslayout.CSSWrap;
+import com.facebook.csslayout.Spacing;
+
+/**
+ * Takes common style properties from JS and applies them to a given {@link CSSNode}.
+ */
+public class BaseCSSPropertyApplicator {
+
+ private static final String PROP_ON_LAYOUT = "onLayout";
+
+ /**
+ * Takes the base props from updateView/manageChildren and applies any CSS styles (if they exist)
+ * to the given {@link CSSNode}.
+ *
+ * TODO(5241893): Add and test border CSS attributes
+ */
+ public static void applyCSSProperties(ReactShadowNode cssNode, CatalystStylesDiffMap props) {
+ if (props.hasKey(ViewProps.WIDTH)) {
+ float width = props.getFloat(ViewProps.WIDTH, CSSConstants.UNDEFINED);
+ cssNode.setStyleWidth(CSSConstants.isUndefined(width) ?
+ width : PixelUtil.toPixelFromDIP(width));
+ }
+
+ if (props.hasKey(ViewProps.HEIGHT)) {
+ float height = props.getFloat(ViewProps.HEIGHT, CSSConstants.UNDEFINED);
+ cssNode.setStyleHeight(CSSConstants.isUndefined(height) ?
+ height : PixelUtil.toPixelFromDIP(height));
+ }
+
+ if (props.hasKey(ViewProps.LEFT)) {
+ float left = props.getFloat(ViewProps.LEFT, CSSConstants.UNDEFINED);
+ cssNode.setPositionLeft(CSSConstants.isUndefined(left) ?
+ left : PixelUtil.toPixelFromDIP(left));
+ }
+
+ if (props.hasKey(ViewProps.TOP)) {
+ float top = props.getFloat(ViewProps.TOP, CSSConstants.UNDEFINED);
+ cssNode.setPositionTop(CSSConstants.isUndefined(top) ?
+ top : PixelUtil.toPixelFromDIP(top));
+ }
+
+ if (props.hasKey(ViewProps.BOTTOM)) {
+ float bottom = props.getFloat(ViewProps.BOTTOM, CSSConstants.UNDEFINED);
+ cssNode.setPositionBottom(CSSConstants.isUndefined(bottom) ?
+ bottom : PixelUtil.toPixelFromDIP(bottom));
+ }
+
+ if (props.hasKey(ViewProps.RIGHT)) {
+ float right = props.getFloat(ViewProps.RIGHT, CSSConstants.UNDEFINED);
+ cssNode.setPositionRight(CSSConstants.isUndefined(right) ?
+ right : PixelUtil.toPixelFromDIP(right));
+ }
+
+ if (props.hasKey(ViewProps.FLEX)) {
+ cssNode.setFlex(props.getFloat(ViewProps.FLEX, 0.f));
+ }
+
+ if (props.hasKey(ViewProps.FLEX_DIRECTION)) {
+ String flexDirectionString = props.getString(ViewProps.FLEX_DIRECTION);
+ cssNode.setFlexDirection(flexDirectionString == null ?
+ CSSFlexDirection.COLUMN : CSSFlexDirection.valueOf(
+ flexDirectionString.toUpperCase(Locale.US)));
+ }
+
+ if (props.hasKey(ViewProps.FLEX_WRAP)) {
+ String flexWrapString = props.getString(ViewProps.FLEX_WRAP);
+ cssNode.setWrap(flexWrapString == null ?
+ CSSWrap.NOWRAP : CSSWrap.valueOf(flexWrapString.toUpperCase(Locale.US)));
+ }
+
+ if (props.hasKey(ViewProps.ALIGN_SELF)) {
+ String alignSelfString = props.getString(ViewProps.ALIGN_SELF);
+ cssNode.setAlignSelf(alignSelfString == null ?
+ CSSAlign.AUTO : CSSAlign.valueOf(
+ alignSelfString.toUpperCase(Locale.US).replace("-", "_")));
+ }
+
+ if (props.hasKey(ViewProps.ALIGN_ITEMS)) {
+ String alignItemsString = props.getString(ViewProps.ALIGN_ITEMS);
+ cssNode.setAlignItems(alignItemsString == null ?
+ CSSAlign.STRETCH : CSSAlign.valueOf(
+ alignItemsString.toUpperCase(Locale.US).replace("-", "_")));
+ }
+
+ if (props.hasKey(ViewProps.JUSTIFY_CONTENT)) {
+ String justifyContentString = props.getString(ViewProps.JUSTIFY_CONTENT);
+ cssNode.setJustifyContent(justifyContentString == null ? CSSJustify.FLEX_START
+ : CSSJustify.valueOf(justifyContentString.toUpperCase(Locale.US).replace("-", "_")));
+ }
+
+ for (int i = 0; i < ViewProps.MARGINS.length; i++) {
+ if (props.hasKey(ViewProps.MARGINS[i])) {
+ cssNode.setMargin(
+ ViewProps.PADDING_MARGIN_SPACING_TYPES[i],
+ PixelUtil.toPixelFromDIP(props.getFloat(ViewProps.MARGINS[i], 0.f)));
+ }
+ }
+
+ for (int i = 0; i < ViewProps.PADDINGS.length; i++) {
+ if (props.hasKey(ViewProps.PADDINGS[i])) {
+ float value = props.getFloat(ViewProps.PADDINGS[i], CSSConstants.UNDEFINED);
+ cssNode.setPadding(
+ ViewProps.PADDING_MARGIN_SPACING_TYPES[i],
+ CSSConstants.isUndefined(value) ? value : PixelUtil.toPixelFromDIP(value));
+ }
+ }
+
+ for (int i = 0; i < ViewProps.BORDER_WIDTHS.length; i++) {
+ if (props.hasKey(ViewProps.BORDER_WIDTHS[i])) {
+ cssNode.setBorder(
+ ViewProps.BORDER_SPACING_TYPES[i],
+ PixelUtil.toPixelFromDIP(props.getFloat(ViewProps.BORDER_WIDTHS[i], 0.f)));
+ }
+ }
+
+ if (props.hasKey(ViewProps.POSITION)) {
+ String positionString = props.getString(ViewProps.POSITION);
+ CSSPositionType positionType = positionString == null ?
+ CSSPositionType.RELATIVE : CSSPositionType.valueOf(positionString.toUpperCase(Locale.US));
+ cssNode.setPositionType(positionType);
+ }
+
+ if (props.hasKey(PROP_ON_LAYOUT)) {
+ cssNode.setShouldNotifyOnLayout(props.getBoolean(PROP_ON_LAYOUT, false));
+ }
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewPropertyApplicator.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewPropertyApplicator.java
new file mode 100644
index 00000000000000..d8f00bfcc8332e
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewPropertyApplicator.java
@@ -0,0 +1,175 @@
+/**
+ * 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.uimanager;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.HashMap;
+
+import android.graphics.Color;
+import android.os.Build;
+import android.view.View;
+import com.facebook.react.bridge.ReadableMap;
+
+/**
+ * Takes common view properties from JS and applies them to a given {@link View}.
+ */
+public class BaseViewPropertyApplicator {
+
+ private static final String PROP_BACKGROUND_COLOR = ViewProps.BACKGROUND_COLOR;
+ private static final String PROP_DECOMPOSED_MATRIX = "decomposedMatrix";
+ private static final String PROP_DECOMPOSED_MATRIX_ROTATE = "rotate";
+ private static final String PROP_DECOMPOSED_MATRIX_SCALE_X = "scaleX";
+ private static final String PROP_DECOMPOSED_MATRIX_SCALE_Y = "scaleY";
+ private static final String PROP_DECOMPOSED_MATRIX_TRANSLATE_X = "translateX";
+ private static final String PROP_DECOMPOSED_MATRIX_TRANSLATE_Y = "translateY";
+ private static final String PROP_OPACITY = "opacity";
+ private static final String PROP_RENDER_TO_HARDWARE_TEXTURE = "renderToHardwareTextureAndroid";
+ private static final String PROP_ACCESSIBILITY_LABEL = "accessibilityLabel";
+ private static final String PROP_ACCESSIBILITY_COMPONENT_TYPE = "accessibilityComponentType";
+ private static final String PROP_ACCESSIBILITY_LIVE_REGION = "accessibilityLiveRegion";
+ private static final String PROP_IMPORTANT_FOR_ACCESSIBILITY = "importantForAccessibility";
+
+ // DEPRECATED
+ private static final String PROP_ROTATION = "rotation";
+ private static final String PROP_SCALE_X = "scaleX";
+ private static final String PROP_SCALE_Y = "scaleY";
+ private static final String PROP_TRANSLATE_X = "translateX";
+ private static final String PROP_TRANSLATE_Y = "translateY";
+
+ /**
+ * Used to locate views in end-to-end (UI) tests.
+ */
+ public static final String PROP_TEST_ID = "testID";
+
+ private static final Map mCommonProps;
+ static {
+ Map props = new HashMap();
+ props.put(PROP_ACCESSIBILITY_LABEL, UIProp.Type.STRING);
+ props.put(PROP_ACCESSIBILITY_COMPONENT_TYPE, UIProp.Type.STRING);
+ props.put(PROP_ACCESSIBILITY_LIVE_REGION, UIProp.Type.STRING);
+ props.put(PROP_BACKGROUND_COLOR, UIProp.Type.STRING);
+ props.put(PROP_IMPORTANT_FOR_ACCESSIBILITY, UIProp.Type.STRING);
+ props.put(PROP_OPACITY, UIProp.Type.NUMBER);
+ props.put(PROP_ROTATION, UIProp.Type.NUMBER);
+ props.put(PROP_SCALE_X, UIProp.Type.NUMBER);
+ props.put(PROP_SCALE_Y, UIProp.Type.NUMBER);
+ props.put(PROP_TRANSLATE_X, UIProp.Type.NUMBER);
+ props.put(PROP_TRANSLATE_Y, UIProp.Type.NUMBER);
+ props.put(PROP_TEST_ID, UIProp.Type.STRING);
+ props.put(PROP_RENDER_TO_HARDWARE_TEXTURE, UIProp.Type.BOOLEAN);
+ mCommonProps = Collections.unmodifiableMap(props);
+ }
+
+ public static Map getCommonProps() {
+ return mCommonProps;
+ }
+
+ public static void applyCommonViewProperties(View view, CatalystStylesDiffMap props) {
+ if (props.hasKey(PROP_BACKGROUND_COLOR)) {
+ String backgroundString = props.getString(PROP_BACKGROUND_COLOR);
+ if (backgroundString == null) {
+ view.setBackgroundColor(Color.TRANSPARENT);
+ } else {
+ view.setBackgroundColor(CSSColorUtil.getColor(backgroundString));
+ }
+ }
+ if (props.hasKey(PROP_DECOMPOSED_MATRIX)) {
+ ReadableMap decomposedMatrix = props.getMap(PROP_DECOMPOSED_MATRIX);
+ if (decomposedMatrix == null) {
+ resetTransformMatrix(view);
+ } else {
+ setTransformMatrix(view, decomposedMatrix);
+ }
+ }
+ if (props.hasKey(PROP_OPACITY)) {
+ view.setAlpha(props.getFloat(PROP_OPACITY, 1.f));
+ }
+ if (props.hasKey(PROP_RENDER_TO_HARDWARE_TEXTURE)) {
+ boolean useHWTexture = props.getBoolean(PROP_RENDER_TO_HARDWARE_TEXTURE, false);
+ view.setLayerType(useHWTexture ? View.LAYER_TYPE_HARDWARE : View.LAYER_TYPE_NONE, null);
+ }
+
+ if (props.hasKey(PROP_TEST_ID)) {
+ view.setTag(props.getString(PROP_TEST_ID));
+ }
+
+ if (props.hasKey(PROP_ACCESSIBILITY_LABEL)) {
+ view.setContentDescription(props.getString(PROP_ACCESSIBILITY_LABEL));
+ }
+ if (props.hasKey(PROP_ACCESSIBILITY_COMPONENT_TYPE)) {
+ AccessibilityHelper.updateAccessibilityComponentType(
+ view,
+ props.getString(PROP_ACCESSIBILITY_COMPONENT_TYPE));
+ }
+ if (props.hasKey(PROP_ACCESSIBILITY_LIVE_REGION)) {
+ if (Build.VERSION.SDK_INT >= 19) {
+ String liveRegionString = props.getString(PROP_ACCESSIBILITY_LIVE_REGION);
+ if (liveRegionString == null || liveRegionString.equals("none")) {
+ view.setAccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_NONE);
+ } else if (liveRegionString.equals("polite")) {
+ view.setAccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE);
+ } else if (liveRegionString.equals("assertive")) {
+ view.setAccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_ASSERTIVE);
+ }
+ }
+ }
+ if (props.hasKey(PROP_IMPORTANT_FOR_ACCESSIBILITY)) {
+ String importantForAccessibility = props.getString(PROP_IMPORTANT_FOR_ACCESSIBILITY);
+ if (importantForAccessibility == null || importantForAccessibility.equals("auto")) {
+ view.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
+ } else if (importantForAccessibility.equals("yes")) {
+ view.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
+ } else if (importantForAccessibility.equals("no")) {
+ view.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
+ } else if (importantForAccessibility.equals("no-hide-descendants")) {
+ view.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
+ }
+ }
+
+ // DEPRECATED
+ if (props.hasKey(PROP_ROTATION)) {
+ view.setRotation(props.getFloat(PROP_ROTATION, 0));
+ }
+ if (props.hasKey(PROP_SCALE_X)) {
+ view.setScaleX(props.getFloat(PROP_SCALE_X, 1.f));
+ }
+ if (props.hasKey(PROP_SCALE_Y)) {
+ view.setScaleY(props.getFloat(PROP_SCALE_Y, 1.f));
+ }
+ if (props.hasKey(PROP_TRANSLATE_X)) {
+ view.setTranslationX(PixelUtil.toPixelFromDIP(props.getFloat(PROP_TRANSLATE_X, 0)));
+ }
+ if (props.hasKey(PROP_TRANSLATE_Y)) {
+ view.setTranslationY(PixelUtil.toPixelFromDIP(props.getFloat(PROP_TRANSLATE_Y, 0)));
+ }
+ }
+
+ private static void setTransformMatrix(View view, ReadableMap matrix) {
+ view.setTranslationX(PixelUtil.toPixelFromDIP(
+ (float) matrix.getDouble(PROP_DECOMPOSED_MATRIX_TRANSLATE_X)));
+ view.setTranslationY(PixelUtil.toPixelFromDIP(
+ (float) matrix.getDouble(PROP_DECOMPOSED_MATRIX_TRANSLATE_Y)));
+ view.setRotation(
+ (float) matrix.getDouble(PROP_DECOMPOSED_MATRIX_ROTATE));
+ view.setScaleX(
+ (float) matrix.getDouble(PROP_DECOMPOSED_MATRIX_SCALE_X));
+ view.setScaleY(
+ (float) matrix.getDouble(PROP_DECOMPOSED_MATRIX_SCALE_Y));
+ }
+
+ private static void resetTransformMatrix(View view) {
+ view.setTranslationX(PixelUtil.toPixelFromDIP(0));
+ view.setTranslationY(PixelUtil.toPixelFromDIP(0));
+ view.setRotation(0);
+ view.setScaleX(1);
+ view.setScaleY(1);
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/CSSColorUtil.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/CSSColorUtil.java
new file mode 100644
index 00000000000000..e61150ba96ce41
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/CSSColorUtil.java
@@ -0,0 +1,155 @@
+/**
+ * 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.uimanager;
+
+import java.util.HashMap;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import android.graphics.Color;
+
+import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
+import com.facebook.react.common.annotations.VisibleForTesting;
+
+/**
+ * Translates the different color formats to their actual colors.
+ */
+public class CSSColorUtil {
+
+ static final Pattern RGB_COLOR_PATTERN =
+ Pattern.compile("rgb\\(\\s*([0-9]{1,3}),\\s*([0-9]{1,3}),\\s*([0-9]{1,3})\\s*\\)");
+
+ static final Pattern RGBA_COLOR_PATTERN = Pattern.compile(
+ "rgba\\(\\s*([0-9]{1,3}),\\s*([0-9]{1,3}),\\s*([0-9]{1,3})\\s*,\\s*(0*(\\.\\d{1,3})?|1(\\.0+)?)\\)");
+
+ private static final HashMap sColorNameMap = new HashMap();
+
+ static {
+ // List of HTML4 colors: http://www.w3.org/TR/css3-color/#html4
+ sColorNameMap.put("black", Color.argb(255, 0, 0, 0));
+ sColorNameMap.put("silver", Color.argb(255, 192, 192, 192));
+ sColorNameMap.put("gray", Color.argb(255, 128, 128, 128));
+ sColorNameMap.put("grey", Color.argb(255, 128, 128, 128));
+ sColorNameMap.put("white", Color.argb(255, 255, 255, 255));
+ sColorNameMap.put("maroon", Color.argb(255, 128, 0, 0));
+ sColorNameMap.put("red", Color.argb(255, 255, 0, 0));
+ sColorNameMap.put("purple", Color.argb(255, 128, 0, 128));
+ sColorNameMap.put("fuchsia", Color.argb(255, 255, 0, 255));
+ sColorNameMap.put("green", Color.argb(255, 0, 128, 0));
+ sColorNameMap.put("lime", Color.argb(255, 0, 255, 0));
+ sColorNameMap.put("olive", Color.argb(255, 128, 128, 0));
+ sColorNameMap.put("yellow", Color.argb(255, 255, 255, 0));
+ sColorNameMap.put("navy", Color.argb(255, 0, 0, 128));
+ sColorNameMap.put("blue", Color.argb(255, 0, 0, 255));
+ sColorNameMap.put("teal", Color.argb(255, 0, 128, 128));
+ sColorNameMap.put("aqua", Color.argb(255, 0, 255, 255));
+
+ // Extended colors
+ sColorNameMap.put("orange", Color.argb(255, 255, 165, 0));
+ sColorNameMap.put("transparent", Color.argb(0, 0, 0, 0));
+ }
+
+ /**
+ * Parses the given color string and returns the corresponding color int value.
+ *
+ * The following color formats are supported:
+ *
+ * - #rgb - Example: "#F02" (will be expanded to "#FF0022")
+ * - #rrggbb - Example: "#FF0022"
+ * - rgb(r, g, b) - Example: "rgb(255, 0, 34)"
+ * - rgba(r, g, b, a) - Example: "rgba(255, 0, 34, 0.2)"
+ * - Color names - Example: "red" or "transparent"
+ *
+ * @param colorString the string representation of the color
+ * @return the color int
+ */
+ public static int getColor(String colorString) {
+ if (colorString.startsWith("rgb(")) {
+ Matcher rgbMatcher = RGB_COLOR_PATTERN.matcher(colorString);
+ if (rgbMatcher.matches()) {
+ return Color.rgb(
+ validateColorComponent(Integer.parseInt(rgbMatcher.group(1))),
+ validateColorComponent(Integer.parseInt(rgbMatcher.group(2))),
+ validateColorComponent(Integer.parseInt(rgbMatcher.group(3))));
+ } else {
+ throw new JSApplicationIllegalArgumentException("Invalid color: " + colorString);
+ }
+ } else if (colorString.startsWith("rgba(")) {
+ Matcher rgbaMatcher = RGBA_COLOR_PATTERN.matcher(colorString);
+ if (rgbaMatcher.matches()) {
+ return Color.argb(
+ (int) (Float.parseFloat(rgbaMatcher.group(4)) * 255),
+ validateColorComponent(Integer.parseInt(rgbaMatcher.group(1))),
+ validateColorComponent(Integer.parseInt(rgbaMatcher.group(2))),
+ validateColorComponent(Integer.parseInt(rgbaMatcher.group(3))));
+ } else {
+ throw new JSApplicationIllegalArgumentException("Invalid color: " + colorString);
+ }
+ } else if (colorString.startsWith("#")) {
+ if (colorString.length() == 4) {
+ int r = parseHexChar(colorString.charAt(1));
+ int g = parseHexChar(colorString.charAt(2));
+ int b = parseHexChar(colorString.charAt(3));
+
+ // double the character
+ // since parseHexChar only returns values from 0-15, we don't need & 0xff
+ r = r | (r << 4);
+ g = g | (g << 4);
+ b = b | (b << 4);
+ return Color.rgb(r, g, b);
+ } else {
+ // check if we have #RRGGBB
+ if (colorString.length() == 7) {
+ // Color.parseColor(...) can throw an IllegalArgumentException("Unknown color").
+ // For consistency, we hide the original exception and throw our own exception instead.
+ try {
+ return Color.parseColor(colorString);
+ } catch (IllegalArgumentException ex) {
+ throw new JSApplicationIllegalArgumentException("Invalid color: " + colorString);
+ }
+ } else {
+ throw new JSApplicationIllegalArgumentException("Invalid color: " + colorString);
+ }
+ }
+ } else {
+ Integer color = sColorNameMap.get(colorString.toLowerCase());
+ if (color != null) {
+ return color;
+ }
+ throw new JSApplicationIllegalArgumentException("Unknown color: " + colorString);
+ }
+ }
+
+ /**
+ * Convert a single hex character (0-9, a-f, A-F) to a number (0-15).
+ *
+ * @param hexChar the hex character to convert
+ * @return the value between 0 and 15
+ */
+ @VisibleForTesting
+ /*package*/ static int parseHexChar(char hexChar) {
+ if (hexChar >= '0' && hexChar <= '9') {
+ return hexChar - '0';
+ } else if (hexChar >= 'A' && hexChar <= 'F') {
+ return hexChar - 'A' + 10;
+ } else if (hexChar >= 'a' && hexChar <= 'f') {
+ return hexChar - 'a' + 10;
+ }
+ throw new JSApplicationIllegalArgumentException("Invalid hex character: " + hexChar);
+ }
+
+ private static int validateColorComponent(int color) {
+ if (color < 0 || color > 255) {
+ throw new JSApplicationIllegalArgumentException("Invalid color component: " + color);
+ }
+ return color;
+ }
+
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/CatalystStylesDiffMap.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/CatalystStylesDiffMap.java
new file mode 100644
index 00000000000000..80bdec21983c9c
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/CatalystStylesDiffMap.java
@@ -0,0 +1,89 @@
+/**
+ * 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.uimanager;
+
+import javax.annotation.Nullable;
+
+import android.view.View;
+
+import com.facebook.react.bridge.ReadableArray;
+import com.facebook.react.bridge.ReadableMap;
+
+/**
+ * Wrapper for {@link ReadableMap} which should be used for styles property map. It extends
+ * some of the accessor methods of {@link ReadableMap} by adding a default value property
+ * such that caller is enforced to provide a default value for a style property.
+ *
+ * Instances of this class are used to update {@link View} or {@link CSSNode} style properties.
+ * Since properties are generated by React framework based on what has been updated each value
+ * in this map should either be interpreted as a new value set for a style property or as a "reset
+ * this property to default" command in case when value is null (this is a way React communicates
+ * change in which the style key that was previously present in a map has been removed).
+ *
+ * NOTE: Accessor method with default value will throw an exception when the key is not present in
+ * the map. Style applicator logic should verify whether the key exists in the map using
+ * {@link #hasKey} before fetching the value. The motivation behind this is that in case when the
+ * updated style diff map doesn't contain a certain style key it means that the corresponding view
+ * property shouldn't be updated (whereas in all other cases it should be updated to the new value
+ * or the property should be reset).
+ */
+public class CatalystStylesDiffMap {
+
+ /* package */ final ReadableMap mBackingMap;
+
+ public CatalystStylesDiffMap(ReadableMap props) {
+ mBackingMap = props;
+ }
+
+ public boolean hasKey(String name) {
+ return mBackingMap.hasKey(name);
+ }
+
+ public boolean isNull(String name) {
+ return mBackingMap.isNull(name);
+ }
+
+ public boolean getBoolean(String name, boolean restoreNullToDefaultValue) {
+ return mBackingMap.isNull(name) ? restoreNullToDefaultValue : mBackingMap.getBoolean(name);
+ }
+
+ public double getDouble(String name, double restoreNullToDefaultValue) {
+ return mBackingMap.isNull(name) ? restoreNullToDefaultValue : mBackingMap.getDouble(name);
+ }
+
+ public float getFloat(String name, float restoreNullToDefaultValue) {
+ return mBackingMap.isNull(name) ?
+ restoreNullToDefaultValue : (float) mBackingMap.getDouble(name);
+ }
+
+ public int getInt(String name, int restoreNullToDefaultValue) {
+ return mBackingMap.isNull(name) ? restoreNullToDefaultValue : (int) mBackingMap.getDouble(name);
+ }
+
+ @Nullable
+ public String getString(String name) {
+ return mBackingMap.getString(name);
+ }
+
+ @Nullable
+ public ReadableArray getArray(String key) {
+ return mBackingMap.getArray(key);
+ }
+
+ @Nullable
+ public ReadableMap getMap(String key) {
+ return mBackingMap.getMap(key);
+ }
+
+ @Override
+ public String toString() {
+ return "{ " + getClass().getSimpleName() + ": " + mBackingMap.toString() + " }";
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/DisplayMetricsHolder.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/DisplayMetricsHolder.java
new file mode 100644
index 00000000000000..18135146eeecbb
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/DisplayMetricsHolder.java
@@ -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.uimanager;
+
+import android.util.DisplayMetrics;
+
+/**
+ * Holds an instance of the current DisplayMetrics so we don't have to thread it through all the
+ * classes that need it.
+ */
+public class DisplayMetricsHolder {
+
+ private static DisplayMetrics sCurrentDisplayMetrics;
+
+ public static void setDisplayMetrics(DisplayMetrics displayMetrics) {
+ sCurrentDisplayMetrics = displayMetrics;
+ }
+
+ public static DisplayMetrics getDisplayMetrics() {
+ return sCurrentDisplayMetrics;
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/GuardedChoreographerFrameCallback.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/GuardedChoreographerFrameCallback.java
new file mode 100644
index 00000000000000..7abbdae8921659
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/GuardedChoreographerFrameCallback.java
@@ -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.uimanager;
+
+import android.view.Choreographer;
+
+import com.facebook.react.bridge.ReactContext;
+
+/**
+ * Abstract base for a Choreographer FrameCallback that should have any RuntimeExceptions it throws
+ * handled by the {@link com.facebook.react.bridge.NativeModuleCallExceptionHandler} registered if
+ * the app is in dev mode.
+ */
+public abstract class GuardedChoreographerFrameCallback implements Choreographer.FrameCallback {
+
+ private final ReactContext mReactContext;
+
+ protected GuardedChoreographerFrameCallback(ReactContext reactContext) {
+ mReactContext = reactContext;
+ }
+
+ @Override
+ public final void doFrame(long frameTimeNanos) {
+ try {
+ doFrameGuarded(frameTimeNanos);
+ } catch (RuntimeException e) {
+ mReactContext.handleException(e);
+ }
+ }
+
+ /**
+ * Like the standard doFrame but RuntimeExceptions will be caught and passed to
+ * {@link com.facebook.react.bridge.ReactContext#handleException(RuntimeException)}.
+ */
+ protected abstract void doFrameGuarded(long frameTimeNanos);
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/IllegalViewOperationException.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/IllegalViewOperationException.java
new file mode 100644
index 00000000000000..d515ef10d07590
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/IllegalViewOperationException.java
@@ -0,0 +1,22 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+package com.facebook.react.uimanager;
+
+import com.facebook.react.bridge.JSApplicationCausedNativeException;
+
+/**
+ * An exception caused by JS requesting the UI manager to perform an illegal view operation.
+ */
+public class IllegalViewOperationException extends JSApplicationCausedNativeException {
+
+ public IllegalViewOperationException(String msg) {
+ super(msg);
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/MeasureSpecAssertions.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/MeasureSpecAssertions.java
new file mode 100644
index 00000000000000..3247709e619aed
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/MeasureSpecAssertions.java
@@ -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.uimanager;
+
+import android.view.View;
+
+/**
+ * Shared utility for asserting on MeasureSpecs.
+ */
+public class MeasureSpecAssertions {
+
+ public static final void assertExplicitMeasureSpec(int widthMeasureSpec, int heightMeasureSpec) {
+ int widthMode = View.MeasureSpec.getMode(widthMeasureSpec);
+ int heightMode = View.MeasureSpec.getMode(heightMeasureSpec);
+
+ if (widthMode == View.MeasureSpec.UNSPECIFIED || heightMode == View.MeasureSpec.UNSPECIFIED) {
+ throw new IllegalStateException(
+ "A catalyst view must have an explicit width and height given to it. This should " +
+ "normally happen as part of the standard catalyst UI framework.");
+ }
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java
new file mode 100644
index 00000000000000..01b220102750e6
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java
@@ -0,0 +1,607 @@
+/**
+ * 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.uimanager;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.NotThreadSafe;
+
+import android.util.SparseArray;
+import android.util.SparseBooleanArray;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.widget.PopupMenu;
+
+import com.facebook.infer.annotation.Assertions;
+import com.facebook.react.animation.Animation;
+import com.facebook.react.animation.AnimationListener;
+import com.facebook.react.animation.AnimationRegistry;
+import com.facebook.react.bridge.Callback;
+import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
+import com.facebook.react.bridge.ReactApplicationContext;
+import com.facebook.react.bridge.ReactContext;
+import com.facebook.react.bridge.ReadableArray;
+import com.facebook.react.bridge.SoftAssertions;
+import com.facebook.react.bridge.UiThreadUtil;
+import com.facebook.react.touch.JSResponderHandler;
+import com.facebook.react.uimanager.events.EventDispatcher;
+
+/**
+ * Delegate of {@link UIManagerModule} that owns the native view hierarchy and mapping between
+ * native view names used in JS and corresponding instances of {@link ViewManager}. The
+ * {@link UIManagerModule} communicates with this class by it's public interface methods:
+ * - {@link #updateProperties}
+ * - {@link #updateLayout}
+ * - {@link #createView}
+ * - {@link #manageChildren}
+ * executing all the scheduled UI operations at the end of JS batch.
+ *
+ * NB: All native view management methods listed above must be called from the UI thread.
+ *
+ * The {@link ReactContext} instance that is passed to views that this manager creates differs
+ * from the one that we pass as a constructor. Instead we wrap the provided instance of
+ * {@link ReactContext} in an instance of {@link ThemedReactContext} that additionally provide
+ * a correct theme based on the root view for a view tree that we attach newly created view to.
+ * Therefore this view manager will create a copy of {@link ThemedReactContext} that wraps
+ * the instance of {@link ReactContext} for each root view added to the manager (see
+ * {@link #addRootView}).
+ *
+ * TODO(5483031): Only dispatch updates when shadow views have changed
+ */
+@NotThreadSafe
+/* package */ final class NativeViewHierarchyManager {
+
+ private final AnimationRegistry mAnimationRegistry;
+ private final SparseArray mTagsToViews;
+ private final SparseArray mTagsToViewManagers;
+ private final SparseBooleanArray mRootTags;
+ private final SparseArray mRootViewsContext;
+ private final ViewManagerRegistry mViewManagers;
+ private final JSResponderHandler mJSResponderHandler = new JSResponderHandler();
+ private final RootViewManager mRootViewManager = new RootViewManager();
+
+ public NativeViewHierarchyManager(
+ AnimationRegistry animationRegistry,
+ ViewManagerRegistry viewManagers) {
+ mAnimationRegistry = animationRegistry;
+ mViewManagers = viewManagers;
+ mTagsToViews = new SparseArray<>();
+ mTagsToViewManagers = new SparseArray<>();
+ mRootTags = new SparseBooleanArray();
+ mRootViewsContext = new SparseArray<>();
+ }
+
+ public void updateProperties(int tag, CatalystStylesDiffMap props) {
+ UiThreadUtil.assertOnUiThread();
+
+ ViewManager viewManager = mTagsToViewManagers.get(tag);
+ if (viewManager == null) {
+ throw new IllegalViewOperationException("ViewManager for tag " + tag + " could not be found");
+ }
+
+ View viewToUpdate = mTagsToViews.get(tag);
+ if (viewToUpdate == null) {
+ throw new IllegalViewOperationException("Trying to update view with tag " + tag
+ + " which doesn't exist");
+ }
+ viewManager.updateView(viewToUpdate, props);
+ }
+
+ public void updateViewExtraData(int tag, Object extraData) {
+ UiThreadUtil.assertOnUiThread();
+
+ ViewManager viewManager = mTagsToViewManagers.get(tag);
+ if (viewManager == null) {
+ throw new IllegalViewOperationException("ViewManager for tag " + tag + " could not be found");
+ }
+
+ View viewToUpdate = mTagsToViews.get(tag);
+ if (viewToUpdate == null) {
+ throw new IllegalViewOperationException("Trying to update view with tag " + tag + " which " +
+ "doesn't exist");
+ }
+ viewManager.updateExtraData(viewToUpdate, extraData);
+ }
+
+ public void updateLayout(
+ int parentTag,
+ int tag,
+ int x,
+ int y,
+ int width,
+ int height) {
+ UiThreadUtil.assertOnUiThread();
+
+ View viewToUpdate = mTagsToViews.get(tag);
+ if (viewToUpdate == null) {
+ throw new IllegalViewOperationException("Trying to update view with tag " + tag + " which " +
+ "doesn't exist");
+ }
+
+ // Even though we have exact dimensions, we still call measure because some platform views (e.g.
+ // Switch) assume that method will always be called before onLayout and onDraw. They use it to
+ // calculate and cache information used in the draw pass. For most views, onMeasure can be
+ // stubbed out to only call setMeasuredDimensions. For ViewGroups, onLayout should be stubbed
+ // out to not recursively call layout on its children: React Native already handles doing that.
+ //
+ // Also, note measure and layout need to be called *after* all View properties have been updated
+ // because of caching and calculation that may occur in onMeasure and onLayout. Layout
+ // operations should also follow the native view hierarchy and go top to bottom for consistency
+ // with standard layout passes (some views may depend on this).
+
+ viewToUpdate.measure(
+ View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
+ View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY));
+
+ // Check if the parent of the view has to layout the view, or the child has to lay itself out.
+ if (!mRootTags.get(parentTag)) {
+ ViewManager parentViewManager = mTagsToViewManagers.get(parentTag);
+ ViewGroupManager parentViewGroupManager;
+ if (parentViewManager instanceof ViewGroupManager) {
+ parentViewGroupManager = (ViewGroupManager) parentViewManager;
+ } else {
+ throw new IllegalViewOperationException("Trying to use view with tag " + tag +
+ " as a parent, but its Manager doesn't extends ViewGroupManager");
+ }
+ if (parentViewGroupManager != null
+ && !parentViewGroupManager.needsCustomLayoutForChildren()) {
+ viewToUpdate.layout(x, y, x + width, y + height);
+ }
+ } else {
+ viewToUpdate.layout(x, y, x + width, y + height);
+ }
+ }
+
+ public void createView(
+ int rootViewTagForContext,
+ int tag,
+ String className,
+ @Nullable CatalystStylesDiffMap initialProps) {
+ UiThreadUtil.assertOnUiThread();
+ ViewManager viewManager = mViewManagers.get(className);
+
+ View view =
+ viewManager.createView(mRootViewsContext.get(rootViewTagForContext), mJSResponderHandler);
+ mTagsToViews.put(tag, view);
+ mTagsToViewManagers.put(tag, viewManager);
+
+ // Use android View id field to store React tag. This is possible since we don't inflate
+ // React views from layout xmls. Thus it is easier to just reuse that field instead of
+ // creating another (potentially much more expensive) mapping from view to React tag
+ view.setId(tag);
+ if (initialProps != null) {
+ viewManager.updateView(view, initialProps);
+ }
+ }
+
+ private static String constructManageChildrenErrorMessage(
+ ViewGroup viewToManage,
+ ViewGroupManager viewManager,
+ @Nullable int[] indicesToRemove,
+ @Nullable ViewAtIndex[] viewsToAdd,
+ @Nullable int[] tagsToDelete) {
+ StringBuilder stringBuilder = new StringBuilder();
+
+ stringBuilder.append("View tag:" + viewToManage.getId() + "\n");
+ stringBuilder.append(" children(" + viewManager.getChildCount(viewToManage) + "): [\n");
+ for (int index=0; index= 0; i--) {
+ int indexToRemove = indicesToRemove[i];
+ if (indexToRemove < 0) {
+ throw new IllegalViewOperationException(
+ "Trying to remove a negative view index:"
+ + indexToRemove + " view tag: " + tag + "\n detail: " +
+ constructManageChildrenErrorMessage(
+ viewToManage,
+ viewManager,
+ indicesToRemove,
+ viewsToAdd,
+ tagsToDelete));
+ }
+ if (indexToRemove >= viewManager.getChildCount(viewToManage)) {
+ throw new IllegalViewOperationException(
+ "Trying to remove a view index above child " +
+ "count " + indexToRemove + " view tag: " + tag + "\n detail: " +
+ constructManageChildrenErrorMessage(
+ viewToManage,
+ viewManager,
+ indicesToRemove,
+ viewsToAdd,
+ tagsToDelete));
+ }
+ if (indexToRemove >= lastIndexToRemove) {
+ throw new IllegalViewOperationException(
+ "Trying to remove an out of order view index:"
+ + indexToRemove + " view tag: " + tag + "\n detail: " +
+ constructManageChildrenErrorMessage(
+ viewToManage,
+ viewManager,
+ indicesToRemove,
+ viewsToAdd,
+ tagsToDelete));
+ }
+ View childView = viewManager.getChildAt(viewToManage, indicesToRemove[i]);
+ if (childView == null) {
+ throw new IllegalViewOperationException(
+ "Trying to remove a null view at index:"
+ + indexToRemove + " view tag: " + tag + "\n detail: " +
+ constructManageChildrenErrorMessage(
+ viewToManage,
+ viewManager,
+ indicesToRemove,
+ viewsToAdd,
+ tagsToDelete));
+ }
+ viewManager.removeView(viewToManage, childView);
+ lastIndexToRemove = indexToRemove;
+ }
+ }
+
+ if (viewsToAdd != null) {
+ for (int i = 0; i < viewsToAdd.length; i++) {
+ ViewAtIndex viewAtIndex = viewsToAdd[i];
+ View viewToAdd = mTagsToViews.get(viewAtIndex.mTag);
+ if (viewToAdd == null) {
+ throw new IllegalViewOperationException(
+ "Trying to add unknown view tag: "
+ + viewAtIndex.mTag + "\n detail: " +
+ constructManageChildrenErrorMessage(
+ viewToManage,
+ viewManager,
+ indicesToRemove,
+ viewsToAdd,
+ tagsToDelete));
+ }
+ viewManager.addView(viewToManage, viewToAdd, viewAtIndex.mIndex);
+ }
+ }
+
+ if (tagsToDelete != null) {
+ for (int i = 0; i < tagsToDelete.length; i++) {
+ int tagToDelete = tagsToDelete[i];
+ View viewToDestroy = mTagsToViews.get(tagToDelete);
+ if (viewToDestroy == null) {
+ throw new IllegalViewOperationException(
+ "Trying to destroy unknown view tag: "
+ + tagToDelete + "\n detail: " +
+ constructManageChildrenErrorMessage(
+ viewToManage,
+ viewManager,
+ indicesToRemove,
+ viewsToAdd,
+ tagsToDelete));
+ }
+ dropView(viewToDestroy);
+ }
+ }
+ }
+
+ /**
+ * See {@link UIManagerModule#addMeasuredRootView}.
+ *
+ * Must be called from the UI thread.
+ */
+ public void addRootView(
+ int tag,
+ SizeMonitoringFrameLayout view,
+ ThemedReactContext themedContext) {
+ UiThreadUtil.assertOnUiThread();
+ if (view.getId() != View.NO_ID) {
+ throw new IllegalViewOperationException(
+ "Trying to add a root view with an explicit id already set. React Native uses " +
+ "the id field to track react tags and will overwrite this field. If that is fine, " +
+ "explicitly overwrite the id field to View.NO_ID before calling addMeasuredRootView.");
+ }
+
+ mTagsToViews.put(tag, view);
+ mTagsToViewManagers.put(tag, mRootViewManager);
+ mRootTags.put(tag, true);
+ mRootViewsContext.put(tag, themedContext);
+ view.setId(tag);
+ }
+
+ /**
+ * Releases all references to given native View.
+ */
+ private void dropView(View view) {
+ UiThreadUtil.assertOnUiThread();
+ if (!mRootTags.get(view.getId())) {
+ // For non-root views we notify viewmanager with {@link ViewManager#onDropInstance}
+ Assertions.assertNotNull(mTagsToViewManagers.get(view.getId())).onDropViewInstance(
+ (ThemedReactContext) view.getContext(),
+ view);
+ }
+ ViewManager viewManager = mTagsToViewManagers.get(view.getId());
+ if (view instanceof ViewGroup && viewManager instanceof ViewGroupManager) {
+ ViewGroup viewGroup = (ViewGroup) view;
+ ViewGroupManager viewGroupManager = (ViewGroupManager) viewManager;
+ for (int i = 0; i < viewGroupManager.getChildCount(viewGroup); i++) {
+ View child = viewGroupManager.getChildAt(viewGroup, i);
+ if (mTagsToViews.get(child.getId()) != null) {
+ dropView(child);
+ }
+ }
+ }
+ mTagsToViews.remove(view.getId());
+ mTagsToViewManagers.remove(view.getId());
+ }
+
+ public void removeRootView(int rootViewTag) {
+ UiThreadUtil.assertOnUiThread();
+ SoftAssertions.assertCondition(
+ mRootTags.get(rootViewTag),
+ "View with tag " + rootViewTag + " is not registered as a root view");
+ View rootView = mTagsToViews.get(rootViewTag);
+ dropView(rootView);
+ mRootTags.delete(rootViewTag);
+ mRootViewsContext.remove(rootViewTag);
+ }
+
+ /**
+ * Returns true on success, false on failure. If successful, after calling, output buffer will be
+ * {x, y, width, height}.
+ */
+ public void measure(int tag, int[] outputBuffer) {
+ UiThreadUtil.assertOnUiThread();
+ View v = mTagsToViews.get(tag);
+ if (v == null) {
+ throw new NoSuchNativeViewException("No native view for " + tag + " currently exists");
+ }
+
+ // Puts x/y in outputBuffer[0]/[1]
+ v.getLocationOnScreen(outputBuffer);
+ outputBuffer[2] = v.getWidth();
+ outputBuffer[3] = v.getHeight();
+ }
+
+ public int findTargetTagForTouch(int reactTag, float touchX, float touchY) {
+ View view = mTagsToViews.get(reactTag);
+ if (view == null) {
+ throw new JSApplicationIllegalArgumentException("Could not find view with tag " + reactTag);
+ }
+ return TouchTargetHelper.findTargetTagForTouch(touchY, touchX, (ViewGroup) view);
+ }
+
+ public void setJSResponder(int reactTag, boolean blockNativeResponder) {
+ SoftAssertions.assertCondition(
+ !mRootTags.get(reactTag),
+ "Cannot block native responder on " + reactTag + " that is a root view");
+ ViewParent viewParent = blockNativeResponder ? mTagsToViews.get(reactTag).getParent() : null;
+ mJSResponderHandler.setJSResponder(reactTag, viewParent);
+ }
+
+ public void clearJSResponder() {
+ mJSResponderHandler.clearJSResponder();
+ }
+
+ /* package */ void startAnimationForNativeView(
+ int reactTag,
+ Animation animation,
+ @Nullable final Callback animationCallback) {
+ UiThreadUtil.assertOnUiThread();
+ View view = mTagsToViews.get(reactTag);
+ final int animationId = animation.getAnimationID();
+ if (view != null) {
+ animation.setAnimationListener(new AnimationListener() {
+ @Override
+ public void onFinished() {
+ Animation removedAnimation = mAnimationRegistry.removeAnimation(animationId);
+
+ // There's a chance that there was already a removeAnimation call enqueued on the main
+ // thread when this callback got enqueued on the main thread, but the Animation class
+ // should handle only calling one of onFinished and onCancel exactly once.
+ Assertions.assertNotNull(removedAnimation, "Animation was already removed somehow!");
+ if (animationCallback != null) {
+ animationCallback.invoke(true);
+ }
+ }
+
+ @Override
+ public void onCancel() {
+ Animation removedAnimation = mAnimationRegistry.removeAnimation(animationId);
+
+ Assertions.assertNotNull(removedAnimation, "Animation was already removed somehow!");
+ if (animationCallback != null) {
+ animationCallback.invoke(false);
+ }
+ }
+ });
+ animation.start(view);
+ } else {
+ // TODO(5712813): cleanup callback in JS callbacks table in case of an error
+ throw new IllegalViewOperationException("View with tag " + reactTag + " not found");
+ }
+ }
+
+ public void dispatchCommand(int reactTag, int commandId, @Nullable ReadableArray args) {
+ UiThreadUtil.assertOnUiThread();
+ View view = mTagsToViews.get(reactTag);
+ if (view == null) {
+ throw new IllegalViewOperationException("Trying to send command to a non-existing view " +
+ "with tag " + reactTag);
+ }
+
+ ViewManager viewManager = mTagsToViewManagers.get(reactTag);
+ if (viewManager == null) {
+ throw new IllegalViewOperationException(
+ "ViewManager for view tag " + reactTag + " could not be found");
+ }
+
+ viewManager.receiveCommand(view, commandId, args);
+ }
+
+ /**
+ * Show a {@link PopupMenu}.
+ *
+ * @param reactTag the tag of the anchor view (the PopupMenu is displayed next to this view); this
+ * needs to be the tag of a native view (shadow views can not be anchors)
+ * @param items the menu items as an array of strings
+ * @param success will be called with the position of the selected item as the first argument, or
+ * no arguments if the menu is dismissed
+ */
+ public void showPopupMenu(int reactTag, ReadableArray items, Callback success) {
+ UiThreadUtil.assertOnUiThread();
+ View anchor = mTagsToViews.get(reactTag);
+ if (anchor == null) {
+ throw new JSApplicationIllegalArgumentException("Could not find view with tag " + reactTag);
+ }
+ PopupMenu popupMenu = new PopupMenu(getReactContextForView(reactTag), anchor);
+
+ Menu menu = popupMenu.getMenu();
+ for (int i = 0; i < items.size(); i++) {
+ menu.add(Menu.NONE, Menu.NONE, i, items.getString(i));
+ }
+
+ PopupMenuCallbackHandler handler = new PopupMenuCallbackHandler(success);
+ popupMenu.setOnMenuItemClickListener(handler);
+ popupMenu.setOnDismissListener(handler);
+
+ popupMenu.show();
+ }
+
+ private static class PopupMenuCallbackHandler implements PopupMenu.OnMenuItemClickListener,
+ PopupMenu.OnDismissListener {
+
+ final Callback mSuccess;
+ boolean mConsumed = false;
+
+ private PopupMenuCallbackHandler(Callback success) {
+ mSuccess = success;
+ }
+
+ @Override
+ public void onDismiss(PopupMenu menu) {
+ if (!mConsumed) {
+ mSuccess.invoke(UIManagerModuleConstants.ACTION_DISMISSED);
+ mConsumed = true;
+ }
+ }
+
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
+ if (!mConsumed) {
+ mSuccess.invoke(UIManagerModuleConstants.ACTION_ITEM_SELECTED, item.getOrder());
+ mConsumed = true;
+ return true;
+ }
+ return false;
+ }
+ }
+
+ /**
+ * @return Themed React context for view with a given {@param reactTag} - in the case of root
+ * view it returns the context from {@link #mRootViewsContext} and all the other cases it gets the
+ * context directly from the view using {@link View#getContext}.
+ */
+ private ThemedReactContext getReactContextForView(int reactTag) {
+ if (mRootTags.get(reactTag)) {
+ return Assertions.assertNotNull(mRootViewsContext.get(reactTag));
+ }
+ View view = mTagsToViews.get(reactTag);
+ if (view == null) {
+ throw new JSApplicationIllegalArgumentException("Could not find view with tag " + reactTag);
+ }
+ return (ThemedReactContext) view.getContext();
+ }
+
+ public void sendAccessibilityEvent(int tag, int eventType) {
+ View view = mTagsToViews.get(tag);
+ if (view == null) {
+ throw new JSApplicationIllegalArgumentException("Could not find view with tag " + tag);
+ }
+ AccessibilityHelper.sendAccessibilityEvent(view, eventType);
+ }
+
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyOptimizer.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyOptimizer.java
new file mode 100644
index 00000000000000..94f6ccab64f784
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyOptimizer.java
@@ -0,0 +1,428 @@
+/**
+ * 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.uimanager;
+
+import javax.annotation.Nullable;
+
+import android.util.SparseBooleanArray;
+
+import com.facebook.infer.annotation.Assertions;
+import com.facebook.react.bridge.ReadableMapKeySeyIterator;
+
+/**
+ * Class responsible for optimizing the native view hierarchy while still respecting the final UI
+ * product specified by JS. Basically, JS sends us a hierarchy of nodes that, while easy to reason
+ * about in JS, are very inefficient to translate directly to native views. This class sits in
+ * between {@link UIManagerModule}, which directly receives view commands from JS, and
+ * {@link UIViewOperationQueue}, which enqueues actual operations on the native view hierarchy. It
+ * is able to take instructions from UIManagerModule and output instructions to the native view
+ * hierarchy that achieve the same displayed UI but with fewer views.
+ *
+ * Currently this class is only used to remove layout-only views, that is to say views that only
+ * affect the positions of their children but do not draw anything themselves. These views are
+ * fairly common because 1) containers are used to do layouting via flexbox and 2) the return of
+ * each Component#render() call in JS must be exactly one view, which means views are often wrapped
+ * in a unnecessary layer of hierarchy.
+ *
+ * This optimization is implemented by keeping track of both the unoptimized JS hierarchy and the
+ * optimized native hierarchy in {@link ReactShadowNode}.
+ *
+ * This optimization is important for view hierarchy depth (which can cause stack overflows during
+ * view traversal for complex apps), memory usage, amount of time spent during GCs,
+ * and time-to-display.
+ *
+ * Some examples of the optimizations this class will do based on commands from JS:
+ * - Create a view with only layout props: a description of that view is created as a
+ * {@link ReactShadowNode} in UIManagerModule, but this class will not output any commands to
+ * create the view in the native view hierarchy.
+ * - Update a layout-only view to have non-layout props: before issuing the updateProperties call
+ * to the native view hierarchy, issue commands to create the view we optimized away move it into
+ * the view hierarchy
+ * - Manage the children of a view: multiple manageChildren calls for various parent views may be
+ * issued to the native view hierarchy depending on where the views being added/removed are
+ * attached in the optimized hierarchy
+ */
+public class NativeViewHierarchyOptimizer {
+
+ private static final boolean ENABLED = true;
+
+ private final UIViewOperationQueue mUIViewOperationQueue;
+ private final ShadowNodeRegistry mShadowNodeRegistry;
+ private final SparseBooleanArray mTagsWithLayoutVisited = new SparseBooleanArray();
+
+ public NativeViewHierarchyOptimizer(
+ UIViewOperationQueue uiViewOperationQueue,
+ ShadowNodeRegistry shadowNodeRegistry) {
+ mUIViewOperationQueue = uiViewOperationQueue;
+ mShadowNodeRegistry = shadowNodeRegistry;
+ }
+
+ /**
+ * Handles a createView call. May or may not actually create a native view.
+ */
+ public void handleCreateView(
+ ReactShadowNode node,
+ int rootViewTag,
+ @Nullable CatalystStylesDiffMap initialProps) {
+ if (!ENABLED) {
+ int tag = node.getReactTag();
+ mUIViewOperationQueue.enqueueCreateView(rootViewTag, tag, node.getViewClass(), initialProps);
+ return;
+ }
+
+ boolean isLayoutOnly = node.getViewClass().equals(ViewProps.VIEW_CLASS_NAME) &&
+ isLayoutOnlyAndCollapsable(initialProps);
+ node.setIsLayoutOnly(isLayoutOnly);
+
+ if (!isLayoutOnly) {
+ mUIViewOperationQueue.enqueueCreateView(
+ rootViewTag,
+ node.getReactTag(),
+ node.getViewClass(),
+ initialProps);
+ }
+ }
+
+ /**
+ * Handles an updateView call. If a view transitions from being layout-only to not (or vice-versa)
+ * this could result in some number of additional createView and manageChildren calls. If the
+ * view is layout only, no updateView call will be dispatched to the native hierarchy.
+ */
+ public void handleUpdateView(
+ ReactShadowNode node,
+ String className,
+ CatalystStylesDiffMap props) {
+ if (!ENABLED) {
+ mUIViewOperationQueue.enqueueUpdateProperties(node.getReactTag(), className, props);
+ return;
+ }
+
+ boolean needsToLeaveLayoutOnly = node.isLayoutOnly() && !isLayoutOnlyAndCollapsable(props);
+ if (needsToLeaveLayoutOnly) {
+ transitionLayoutOnlyViewToNativeView(node, props);
+ } else if (!node.isLayoutOnly()) {
+ mUIViewOperationQueue.enqueueUpdateProperties(node.getReactTag(), className, props);
+ }
+ }
+
+ /**
+ * Handles a manageChildren call. This may translate into multiple manageChildren calls for
+ * multiple other views.
+ *
+ * NB: the assumption for calling this method is that all corresponding ReactShadowNodes have
+ * been updated **but tagsToDelete have NOT been deleted yet**. This is because we need to use
+ * the metadata from those nodes to figure out the correct commands to dispatch. This is unlike
+ * all other calls on this class where we assume all operations on the shadow hierarchy have
+ * already completed by the time a corresponding method here is called.
+ */
+ public void handleManageChildren(
+ ReactShadowNode nodeToManage,
+ int[] indicesToRemove,
+ int[] tagsToRemove,
+ ViewAtIndex[] viewsToAdd,
+ int[] tagsToDelete) {
+ if (!ENABLED) {
+ mUIViewOperationQueue.enqueueManageChildren(
+ nodeToManage.getReactTag(),
+ indicesToRemove,
+ viewsToAdd,
+ tagsToDelete);
+ return;
+ }
+
+ // We operate on tagsToRemove instead of indicesToRemove because by the time this method is
+ // called, these views have already been removed from the shadow hierarchy and the indices are
+ // no longer useful to operate on
+ for (int i = 0; i < tagsToRemove.length; i++) {
+ int tagToRemove = tagsToRemove[i];
+ boolean delete = false;
+ for (int j = 0; j < tagsToDelete.length; j++) {
+ if (tagsToDelete[j] == tagToRemove) {
+ delete = true;
+ break;
+ }
+ }
+ ReactShadowNode nodeToRemove = mShadowNodeRegistry.getNode(tagToRemove);
+ removeNodeFromParent(nodeToRemove, delete);
+ }
+
+ for (int i = 0; i < viewsToAdd.length; i++) {
+ ViewAtIndex toAdd = viewsToAdd[i];
+ ReactShadowNode nodeToAdd = mShadowNodeRegistry.getNode(toAdd.mTag);
+ addNodeToNode(nodeToManage, nodeToAdd, toAdd.mIndex);
+ }
+ }
+
+ /**
+ * Handles an updateLayout call. All updateLayout calls are collected and dispatched at the end
+ * of a batch because updateLayout calls to layout-only nodes can necessitate multiple
+ * updateLayout calls for all its children.
+ */
+ public void handleUpdateLayout(ReactShadowNode node) {
+ if (!ENABLED) {
+ mUIViewOperationQueue.enqueueUpdateLayout(
+ Assertions.assertNotNull(node.getParent()).getReactTag(),
+ node.getReactTag(),
+ node.getScreenX(),
+ node.getScreenY(),
+ node.getScreenWidth(),
+ node.getScreenHeight());
+ return;
+ }
+
+ applyLayoutBase(node);
+ }
+
+ /**
+ * Processes the shadow hierarchy to dispatch all necessary updateLayout calls to the native
+ * hierarchy. Should be called after all updateLayout calls for a batch have been handled.
+ */
+ public void onBatchComplete() {
+ mTagsWithLayoutVisited.clear();
+ }
+
+ private void addNodeToNode(ReactShadowNode parent, ReactShadowNode child, int index) {
+ int indexInNativeChildren = parent.getNativeOffsetForChild(parent.getChildAt(index));
+ boolean parentIsLayoutOnly = parent.isLayoutOnly();
+ boolean childIsLayoutOnly = child.isLayoutOnly();
+
+ // Switch on the four cases of:
+ // add (layout-only|not layout-only) to (layout-only|not layout-only)
+ if (!parentIsLayoutOnly && !childIsLayoutOnly) {
+ addNonLayoutNodeToNonLayoutNode(parent, child, indexInNativeChildren);
+ } else if (!childIsLayoutOnly) {
+ addNonLayoutOnlyNodeToLayoutOnlyNode(parent, child, indexInNativeChildren);
+ } else if (!parentIsLayoutOnly) {
+ addLayoutOnlyNodeToNonLayoutOnlyNode(parent, child, indexInNativeChildren);
+ } else {
+ addLayoutOnlyNodeToLayoutOnlyNode(parent, child, indexInNativeChildren);
+ }
+ }
+
+ /**
+ * For handling node removal from manageChildren. In the case of removing a layout-only node, we
+ * need to instead recursively remove all its children from their native parents.
+ */
+ private void removeNodeFromParent(ReactShadowNode nodeToRemove, boolean shouldDelete) {
+ ReactShadowNode nativeNodeToRemoveFrom = nodeToRemove.getNativeParent();
+
+ if (nativeNodeToRemoveFrom != null) {
+ int index = nativeNodeToRemoveFrom.indexOfNativeChild(nodeToRemove);
+ nativeNodeToRemoveFrom.removeNativeChildAt(index);
+
+ mUIViewOperationQueue.enqueueManageChildren(
+ nativeNodeToRemoveFrom.getReactTag(),
+ new int[]{index},
+ null,
+ shouldDelete ? new int[]{nodeToRemove.getReactTag()} : null);
+ } else {
+ for (int i = nodeToRemove.getChildCount() - 1; i >= 0; i--) {
+ removeNodeFromParent(nodeToRemove.getChildAt(i), shouldDelete);
+ }
+ }
+ }
+
+ private void addLayoutOnlyNodeToLayoutOnlyNode(
+ ReactShadowNode parent,
+ ReactShadowNode child,
+ int index) {
+ ReactShadowNode parentParent = parent.getParent();
+
+ // If the parent hasn't been attached to its parent yet, don't issue commands to the native
+ // hierarchy. We'll do that when the parent node actually gets attached somewhere.
+ if (parentParent == null) {
+ return;
+ }
+
+ int transformedIndex = index + parentParent.getNativeOffsetForChild(parent);
+ if (parentParent.isLayoutOnly()) {
+ addLayoutOnlyNodeToLayoutOnlyNode(parentParent, child, transformedIndex);
+ } else {
+ addLayoutOnlyNodeToNonLayoutOnlyNode(parentParent, child, transformedIndex);
+ }
+ }
+
+ private void addNonLayoutOnlyNodeToLayoutOnlyNode(
+ ReactShadowNode layoutOnlyNode,
+ ReactShadowNode nonLayoutOnlyNode,
+ int index) {
+ ReactShadowNode parent = layoutOnlyNode.getParent();
+
+ // If the parent hasn't been attached to its parent yet, don't issue commands to the native
+ // hierarchy. We'll do that when the parent node actually gets attached somewhere.
+ if (parent == null) {
+ return;
+ }
+
+ int transformedIndex = index + parent.getNativeOffsetForChild(layoutOnlyNode);
+ if (parent.isLayoutOnly()) {
+ addNonLayoutOnlyNodeToLayoutOnlyNode(parent, nonLayoutOnlyNode, transformedIndex);
+ } else {
+ addNonLayoutNodeToNonLayoutNode(parent, nonLayoutOnlyNode, transformedIndex);
+ }
+ }
+
+ private void addLayoutOnlyNodeToNonLayoutOnlyNode(
+ ReactShadowNode nonLayoutOnlyNode,
+ ReactShadowNode layoutOnlyNode,
+ int index) {
+ // Add all of the layout-only node's children to its parent instead
+ int currentIndex = index;
+ for (int i = 0; i < layoutOnlyNode.getChildCount(); i++) {
+ ReactShadowNode childToAdd = layoutOnlyNode.getChildAt(i);
+ Assertions.assertCondition(childToAdd.getNativeParent() == null);
+
+ if (childToAdd.isLayoutOnly()) {
+ // Adding this layout-only child could result in adding multiple native views
+ int childCountBefore = nonLayoutOnlyNode.getNativeChildCount();
+ addLayoutOnlyNodeToNonLayoutOnlyNode(
+ nonLayoutOnlyNode,
+ childToAdd,
+ currentIndex);
+ int childCountAfter = nonLayoutOnlyNode.getNativeChildCount();
+ currentIndex += childCountAfter - childCountBefore;
+ } else {
+ addNonLayoutNodeToNonLayoutNode(nonLayoutOnlyNode, childToAdd, currentIndex);
+ currentIndex++;
+ }
+ }
+ }
+
+ private void addNonLayoutNodeToNonLayoutNode(
+ ReactShadowNode parent,
+ ReactShadowNode child,
+ int index) {
+ parent.addNativeChildAt(child, index);
+ mUIViewOperationQueue.enqueueManageChildren(
+ parent.getReactTag(),
+ null,
+ new ViewAtIndex[]{new ViewAtIndex(child.getReactTag(), index)},
+ null);
+ }
+
+ private void applyLayoutBase(ReactShadowNode node) {
+ int tag = node.getReactTag();
+ if (mTagsWithLayoutVisited.get(tag)) {
+ return;
+ }
+ mTagsWithLayoutVisited.put(tag, true);
+
+ ReactShadowNode parent = node.getParent();
+
+ // We use screenX/screenY (which round to integer pixels) at each node in the hierarchy to
+ // emulate what the layout would look like if it were actually built with native views which
+ // have to have integral top/left/bottom/right values
+ int x = node.getScreenX();
+ int y = node.getScreenY();
+
+ while (parent != null && parent.isLayoutOnly()) {
+ // TODO(7854667): handle and test proper clipping
+ x += Math.round(parent.getLayoutX());
+ y += Math.round(parent.getLayoutY());
+
+ parent = parent.getParent();
+ }
+
+ applyLayoutRecursive(node, x, y);
+ }
+
+ private void applyLayoutRecursive(ReactShadowNode toUpdate, int x, int y) {
+ if (!toUpdate.isLayoutOnly() && toUpdate.getNativeParent() != null) {
+ int tag = toUpdate.getReactTag();
+ mUIViewOperationQueue.enqueueUpdateLayout(
+ toUpdate.getNativeParent().getReactTag(),
+ tag,
+ x,
+ y,
+ toUpdate.getScreenWidth(),
+ toUpdate.getScreenHeight());
+ return;
+ }
+
+ for (int i = 0; i < toUpdate.getChildCount(); i++) {
+ ReactShadowNode child = toUpdate.getChildAt(i);
+ int childTag = child.getReactTag();
+ if (mTagsWithLayoutVisited.get(childTag)) {
+ continue;
+ }
+ mTagsWithLayoutVisited.put(childTag, true);
+
+ int childX = child.getScreenX();
+ int childY = child.getScreenY();
+
+ childX += x;
+ childY += y;
+
+ applyLayoutRecursive(child, childX, childY);
+ }
+ }
+
+ private void transitionLayoutOnlyViewToNativeView(
+ ReactShadowNode node,
+ @Nullable CatalystStylesDiffMap props) {
+ ReactShadowNode parent = node.getParent();
+ if (parent == null) {
+ node.setIsLayoutOnly(false);
+ return;
+ }
+
+ // First, remove the node from its parent. This causes the parent to update its native children
+ // count. The removeNodeFromParent call will cause all the view's children to be detached from
+ // their native parent.
+ int childIndex = parent.indexOf(node);
+ parent.removeChildAt(childIndex);
+ removeNodeFromParent(node, false);
+
+ node.setIsLayoutOnly(false);
+
+ // Create the view since it doesn't exist in the native hierarchy yet
+ mUIViewOperationQueue.enqueueCreateView(
+ node.getRootNode().getReactTag(),
+ node.getReactTag(),
+ node.getViewClass(),
+ props);
+
+ // Add the node and all its children as if we are adding a new nodes
+ parent.addChildAt(node, childIndex);
+ addNodeToNode(parent, node, childIndex);
+ for (int i = 0; i < node.getChildCount(); i++) {
+ addNodeToNode(node, node.getChildAt(i), i);
+ }
+
+ // Update layouts since the children of the node were offset by its x/y position previously.
+ // Bit of a hack: we need to update the layout of this node's children now that it's no longer
+ // layout-only, but we may still receive more layout updates at the end of this batch that we
+ // don't want to ignore.
+ Assertions.assertCondition(mTagsWithLayoutVisited.size() == 0);
+ applyLayoutBase(node);
+ for (int i = 0; i < node.getChildCount(); i++) {
+ applyLayoutBase(node.getChildAt(i));
+ }
+ mTagsWithLayoutVisited.clear();
+ }
+
+ private static boolean isLayoutOnlyAndCollapsable(@Nullable CatalystStylesDiffMap props) {
+ if (props == null) {
+ return true;
+ }
+
+ if (props.hasKey(ViewProps.COLLAPSABLE) && !props.getBoolean(ViewProps.COLLAPSABLE, true)) {
+ return false;
+ }
+
+ ReadableMapKeySeyIterator keyIterator = props.mBackingMap.keySetIterator();
+ while (keyIterator.hasNextKey()) {
+ if (!ViewProps.isLayoutOnly(keyIterator.nextKey())) {
+ return false;
+ }
+ }
+ return true;
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/NoSuchNativeViewException.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/NoSuchNativeViewException.java
new file mode 100644
index 00000000000000..d8c125a72a6ed8
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/NoSuchNativeViewException.java
@@ -0,0 +1,21 @@
+/**
+ * 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.uimanager;
+
+/**
+ * Exception thrown when a class tries to access a native view by a tag that has no native view
+ * associated with it.
+ */
+public class NoSuchNativeViewException extends IllegalViewOperationException {
+
+ public NoSuchNativeViewException(String detailMessage) {
+ super(detailMessage);
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/OnLayoutEvent.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/OnLayoutEvent.java
new file mode 100644
index 00000000000000..f553858bd37661
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/OnLayoutEvent.java
@@ -0,0 +1,51 @@
+/**
+ * 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.uimanager;
+
+import com.facebook.react.bridge.Arguments;
+import com.facebook.react.bridge.WritableMap;
+import com.facebook.react.uimanager.events.Event;
+import com.facebook.react.uimanager.events.RCTEventEmitter;
+
+/**
+ * Event used to notify JS component about changes of its position or dimensions
+ */
+/* package */ class OnLayoutEvent extends Event {
+
+ private final int mX, mY, mWidth, mHeight;
+
+ protected OnLayoutEvent(int viewTag, int x, int y, int width, int height) {
+ super(viewTag, 0);
+ mX = x;
+ mY = y;
+ mWidth = width;
+ mHeight = height;
+ }
+
+ @Override
+ public String getEventName() {
+ return "topLayout";
+ }
+
+ @Override
+ public void dispatch(RCTEventEmitter rctEventEmitter) {
+ WritableMap layout = Arguments.createMap();
+ layout.putDouble("x", PixelUtil.toDIPFromPixel(mX));
+ layout.putDouble("y", PixelUtil.toDIPFromPixel(mY));
+ layout.putDouble("width", PixelUtil.toDIPFromPixel(mWidth));
+ layout.putDouble("height", PixelUtil.toDIPFromPixel(mHeight));
+
+ WritableMap event = Arguments.createMap();
+ event.putMap("layout", layout);
+ event.putInt("target", getViewTag());
+
+ rctEventEmitter.receiveEvent(getViewTag(), getEventName(), event);
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/PixelUtil.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/PixelUtil.java
new file mode 100644
index 00000000000000..bcb24667fc5dd6
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/PixelUtil.java
@@ -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.uimanager;
+
+import android.util.TypedValue;
+
+/**
+ * Android dp to pixel manipulation
+ */
+public class PixelUtil {
+
+ /**
+ * Convert from DIP to PX
+ */
+ public static float toPixelFromDIP(float value) {
+ return TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_DIP,
+ value,
+ DisplayMetricsHolder.getDisplayMetrics());
+ }
+
+ /**
+ * Convert from DIP to PX
+ */
+ public static float toPixelFromDIP(double value) {
+ return toPixelFromDIP((float) value);
+ }
+
+ /**
+ * Convert from SP to PX
+ */
+ public static float toPixelFromSP(float value) {
+ return TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_SP,
+ value,
+ DisplayMetricsHolder.getDisplayMetrics());
+ }
+
+ /**
+ * Convert from SP to PX
+ */
+ public static float toPixelFromSP(double value) {
+ return toPixelFromSP((float) value);
+ }
+
+ /**
+ * Convert from PX to DP
+ */
+ public static float toDIPFromPixel(float value) {
+ return value / DisplayMetricsHolder.getDisplayMetrics().density;
+ }
+
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/PointerEvents.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/PointerEvents.java
new file mode 100644
index 00000000000000..1d86fe7494c57c
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/PointerEvents.java
@@ -0,0 +1,38 @@
+/**
+ * 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.uimanager;
+
+/**
+ * Possible values for pointer events that a view and its descendants should receive. See
+ * https://developer.mozilla.org/en-US/docs/Web/CSS/pointer-events for more info.
+ */
+public enum PointerEvents {
+
+ /**
+ * Neither the container nor its children receive events.
+ */
+ NONE,
+
+ /**
+ * Container doesn't get events but all of its children do.
+ */
+ BOX_NONE,
+
+ /**
+ * Container gets events but none of its children do.
+ */
+ BOX_ONLY,
+
+ /**
+ * Container and all of its children receive touch events (like pointerEvents is unspecified).
+ */
+ AUTO,
+ ;
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactChoreographer.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactChoreographer.java
new file mode 100644
index 00000000000000..5b23ceda7fb8f9
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactChoreographer.java
@@ -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.uimanager;
+
+import java.util.ArrayDeque;
+
+import android.view.Choreographer;
+
+import com.facebook.common.logging.FLog;
+import com.facebook.react.bridge.UiThreadUtil;
+import com.facebook.infer.annotation.Assertions;
+import com.facebook.react.common.ReactConstants;
+
+/**
+ * A simple wrapper around Choreographer that allows us to control the order certain callbacks
+ * are executed within a given frame. The main difference is that we enforce this is accessed from
+ * the UI thread: this is because this ordering cannot be guaranteed across multiple threads.
+ */
+public class ReactChoreographer {
+
+ public static enum CallbackType {
+ /**
+ * For use by {@link com.facebook.react.uimanager.UIManagerModule}
+ */
+ DISPATCH_UI(0),
+
+ /**
+ * Events that make JS do things.
+ */
+ TIMERS_EVENTS(1),
+ ;
+
+ private final int mOrder;
+
+ private CallbackType(int order) {
+ mOrder = order;
+ }
+
+ /*package*/ int getOrder() {
+ return mOrder;
+ }
+ }
+
+ private static ReactChoreographer sInstance;
+
+ public static ReactChoreographer getInstance() {
+ UiThreadUtil.assertOnUiThread();
+ if (sInstance == null) {
+ sInstance = new ReactChoreographer();
+ }
+ return sInstance;
+ }
+
+ private final Choreographer mChoreographer;
+ private final ReactChoreographerDispatcher mReactChoreographerDispatcher;
+ private final ArrayDeque[] mCallbackQueues;
+
+ private int mTotalCallbacks = 0;
+ private boolean mHasPostedCallback = false;
+
+ private ReactChoreographer() {
+ mChoreographer = Choreographer.getInstance();
+ mReactChoreographerDispatcher = new ReactChoreographerDispatcher();
+ mCallbackQueues = new ArrayDeque[CallbackType.values().length];
+ for (int i = 0; i < mCallbackQueues.length; i++) {
+ mCallbackQueues[i] = new ArrayDeque<>();
+ }
+ }
+
+ public void postFrameCallback(CallbackType type, Choreographer.FrameCallback frameCallback) {
+ UiThreadUtil.assertOnUiThread();
+ mCallbackQueues[type.getOrder()].addLast(frameCallback);
+ mTotalCallbacks++;
+ Assertions.assertCondition(mTotalCallbacks > 0);
+ if (!mHasPostedCallback) {
+ mChoreographer.postFrameCallback(mReactChoreographerDispatcher);
+ mHasPostedCallback = true;
+ }
+ }
+
+ public void removeFrameCallback(CallbackType type, Choreographer.FrameCallback frameCallback) {
+ UiThreadUtil.assertOnUiThread();
+ if (mCallbackQueues[type.getOrder()].removeFirstOccurrence(frameCallback)) {
+ mTotalCallbacks--;
+ maybeRemoveFrameCallback();
+ } else {
+ FLog.e(ReactConstants.TAG, "Tried to remove non-existent frame callback");
+ }
+ }
+
+ private void maybeRemoveFrameCallback() {
+ Assertions.assertCondition(mTotalCallbacks >= 0);
+ if (mTotalCallbacks == 0 && mHasPostedCallback) {
+ mChoreographer.removeFrameCallback(mReactChoreographerDispatcher);
+ mHasPostedCallback = false;
+ }
+ }
+
+ private class ReactChoreographerDispatcher implements Choreographer.FrameCallback {
+
+ @Override
+ public void doFrame(long frameTimeNanos) {
+ mHasPostedCallback = false;
+ for (int i = 0; i < mCallbackQueues.length; i++) {
+ int initialLength = mCallbackQueues[i].size();
+ for (int callback = 0; callback < initialLength; callback++) {
+ mCallbackQueues[i].removeFirst().doFrame(frameTimeNanos);
+ mTotalCallbacks--;
+ }
+ }
+ maybeRemoveFrameCallback();
+ }
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactCompoundView.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactCompoundView.java
new file mode 100644
index 00000000000000..13abb16e226a56
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactCompoundView.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+package com.facebook.react.uimanager;
+
+import android.view.MotionEvent;
+import android.view.View;
+
+/**
+ * This interface should be implemented be native {@link View} subclasses that can represent more
+ * than a single react node (e.g. TextView). It is use by touch event emitter for determining the
+ * react tag of the inner-view element that was touched.
+ */
+public interface ReactCompoundView {
+
+ /**
+ * Return react tag for touched element. Event coordinates are relative to the view
+ * @param touchX the X touch coordinate relative to the view
+ * @param touchY the Y touch coordinate relative to the view
+ */
+ int reactTagForTouch(float touchX, float touchY);
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactInvalidPropertyException.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactInvalidPropertyException.java
new file mode 100644
index 00000000000000..e219f18ccd0857
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactInvalidPropertyException.java
@@ -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.uimanager;
+
+public class ReactInvalidPropertyException extends RuntimeException {
+
+ public ReactInvalidPropertyException(String property, String value, String expectedValues) {
+ super("Invalid React property `" + property + "` with value `" + value +
+ "`, expected " + expectedValues);
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactNative.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactNative.java
new file mode 100644
index 00000000000000..2d99e79f5698d4
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactNative.java
@@ -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.uimanager;
+
+import com.facebook.react.bridge.JavaScriptModule;
+
+/**
+ * JS module interface - used by UIManager to communicate with main React JS module methods
+ */
+public interface ReactNative extends JavaScriptModule {
+ void unmountComponentAtNodeAndRemoveContainer(int rootNodeTag);
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactPointerEventsView.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactPointerEventsView.java
new file mode 100644
index 00000000000000..e47e13e483cb21
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactPointerEventsView.java
@@ -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.uimanager;
+
+import android.view.View;
+
+/**
+ * This interface should be implemented be native {@link View} subclasses that support pointer
+ * events handling. It is used to find the target View of a touch event.
+ */
+public interface ReactPointerEventsView {
+
+ /**
+ * Return the PointerEvents of the View.
+ */
+ PointerEvents getPointerEvents();
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactShadowNode.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactShadowNode.java
new file mode 100644
index 00000000000000..c4d5aa7b7a18d1
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactShadowNode.java
@@ -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.uimanager;
+
+import javax.annotation.Nullable;
+
+import java.util.ArrayList;
+
+import com.facebook.csslayout.CSSNode;
+import com.facebook.infer.annotation.Assertions;
+
+/**
+ * Base node class for representing virtual tree of React nodes. Shadow nodes are used primarily
+ * for layouting therefore it extends {@link CSSNode} to allow that. They also help with handling
+ * Common base subclass of {@link CSSNode} for all layout nodes for react-based view. It extends
+ * {@link CSSNode} by adding additional capabilities.
+ *
+ * Instances of this class receive property updates from JS via @{link UIManagerModule}. Subclasses
+ * may use {@link #updateProperties} to persist some of the updated fields in the node instance that
+ * corresponds to a particular view type.
+ *
+ * Subclasses of {@link ReactShadowNode} should be created only from {@link ViewManager} that
+ * corresponds to a certain type of native view. They will be updated and accessed only from JS
+ * thread. Subclasses of {@link ViewManager} may choose to use base class {@link ReactShadowNode} or
+ * custom subclass of it if necessary.
+ *
+ * The primary use-case for {@link ReactShadowNode} nodes is to calculate layouting. Although this
+ * might be extended. For some examples please refer to ARTGroupCSSNode or ReactTextCSSNode.
+ *
+ * This class allows for the native view hierarchy to not be an exact copy of the hierarchy received
+ * from JS by keeping track of both JS children (e.g. {@link #getChildCount()} and separately native
+ * children (e.g. {@link #getNativeChildCount()}). See {@link NativeViewHierarchyOptimizer} for more
+ * information.
+ */
+public class ReactShadowNode extends CSSNode {
+
+ private int mReactTag;
+ private @Nullable String mViewClassName;
+ private @Nullable ReactShadowNode mRootNode;
+ private @Nullable ThemedReactContext mThemedContext;
+ private boolean mShouldNotifyOnLayout;
+ private boolean mNodeUpdated = true;
+
+ // layout-only nodes
+ private boolean mIsLayoutOnly;
+ private int mTotalNativeChildren = 0;
+ private @Nullable ReactShadowNode mNativeParent;
+ private @Nullable ArrayList mNativeChildren;
+ private float mAbsoluteLeft;
+ private float mAbsoluteTop;
+ private float mAbsoluteRight;
+ private float mAbsoluteBottom;
+
+ /**
+ * Nodes that return {@code true} will be treated as "virtual" nodes. That is, nodes that are not
+ * mapped into native views (e.g. nested text node). By default this method returns {@code false}.
+ */
+ public boolean isVirtual() {
+ return false;
+ }
+
+ /**
+ * Nodes that return {@code true} will be treated as a root view for the virtual nodes tree. It
+ * means that {@link NativeViewHierarchyManager} will not try to perform {@code manageChildren}
+ * operation on such views. Good example is {@code InputText} view that may have children
+ * {@code Text} nodes but this whole hierarchy will be mapped to a single android {@link EditText}
+ * view.
+ */
+ public boolean isVirtualAnchor() {
+ return false;
+ }
+
+ public final String getViewClass() {
+ return Assertions.assertNotNull(mViewClassName);
+ }
+
+ public final boolean hasUpdates() {
+ return mNodeUpdated || hasNewLayout() || isDirty();
+ }
+
+ public final void markUpdateSeen() {
+ mNodeUpdated = false;
+ if (hasNewLayout()) {
+ markLayoutSeen();
+ }
+ }
+
+ protected void markUpdated() {
+ if (mNodeUpdated) {
+ return;
+ }
+ mNodeUpdated = true;
+ ReactShadowNode parent = getParent();
+ if (parent != null) {
+ parent.markUpdated();
+ }
+ }
+
+ @Override
+ protected void dirty() {
+ if (!isVirtual()) {
+ super.dirty();
+ }
+ }
+
+ @Override
+ public void addChildAt(CSSNode child, int i) {
+ super.addChildAt(child, i);
+ markUpdated();
+ ReactShadowNode node = (ReactShadowNode) child;
+
+ int increase = node.mIsLayoutOnly ? node.mTotalNativeChildren : 1;
+ mTotalNativeChildren += increase;
+
+ if (mIsLayoutOnly) {
+ ReactShadowNode parent = getParent();
+ while (parent != null) {
+ parent.mTotalNativeChildren += increase;
+ if (!parent.mIsLayoutOnly) {
+ break;
+ }
+ parent = parent.getParent();
+ }
+ }
+ }
+
+ @Override
+ public ReactShadowNode removeChildAt(int i) {
+ ReactShadowNode removed = (ReactShadowNode) super.removeChildAt(i);
+ markUpdated();
+
+ int decrease = removed.mIsLayoutOnly ? removed.mTotalNativeChildren : 1;
+ mTotalNativeChildren -= decrease;
+ if (mIsLayoutOnly) {
+ ReactShadowNode parent = getParent();
+ while (parent != null) {
+ parent.mTotalNativeChildren -= decrease;
+ if (!parent.mIsLayoutOnly) {
+ break;
+ }
+ parent = parent.getParent();
+ }
+ }
+ return removed;
+ }
+
+ /**
+ * This method will be called by {@link UIManagerModule} once per batch, before calculating
+ * layout. Will be only called for nodes that are marked as updated with {@link #markUpdated()}
+ * or require layouting (marked with {@link #dirty()}).
+ */
+ public void onBeforeLayout() {
+ }
+
+ public void updateProperties(CatalystStylesDiffMap styles) {
+ BaseCSSPropertyApplicator.applyCSSProperties(this, styles);
+ }
+
+ /**
+ * Called after layout step at the end of the UI batch from {@link UIManagerModule}. May be used
+ * to enqueue additional ui operations for the native view. Will only be called on nodes marked
+ * as updated either with {@link #dirty()} or {@link #markUpdated()}.
+ *
+ * @param uiViewOperationQueue interface for enqueueing UI operations
+ */
+ public void onCollectExtraUpdates(UIViewOperationQueue uiViewOperationQueue) {
+ }
+
+ /* package */ void dispatchUpdates(
+ float absoluteX,
+ float absoluteY,
+ UIViewOperationQueue uiViewOperationQueue,
+ NativeViewHierarchyOptimizer nativeViewHierarchyOptimizer) {
+ if (mNodeUpdated) {
+ onCollectExtraUpdates(uiViewOperationQueue);
+ }
+
+ if (hasNewLayout()) {
+ mAbsoluteLeft = Math.round(absoluteX + getLayoutX());
+ mAbsoluteTop = Math.round(absoluteY + getLayoutY());
+ mAbsoluteRight = Math.round(absoluteX + getLayoutX() + getLayoutWidth());
+ mAbsoluteBottom = Math.round(absoluteY + getLayoutY() + getLayoutHeight());
+
+ nativeViewHierarchyOptimizer.handleUpdateLayout(this);
+ }
+ }
+
+ public final int getReactTag() {
+ return mReactTag;
+ }
+
+ /* package */ final void setReactTag(int reactTag) {
+ mReactTag = reactTag;
+ }
+
+ public final ReactShadowNode getRootNode() {
+ return Assertions.assertNotNull(mRootNode);
+ }
+
+ /* package */ final void setRootNode(ReactShadowNode rootNode) {
+ mRootNode = rootNode;
+ }
+
+ /* package */ final void setViewClassName(String viewClassName) {
+ mViewClassName = viewClassName;
+ }
+
+ @Override
+ public final ReactShadowNode getChildAt(int i) {
+ return (ReactShadowNode) super.getChildAt(i);
+ }
+
+ @Override
+ public final @Nullable ReactShadowNode getParent() {
+ return (ReactShadowNode) super.getParent();
+ }
+
+ /**
+ * Get the {@link ThemedReactContext} associated with this {@link ReactShadowNode}. This will
+ * never change during the lifetime of a {@link ReactShadowNode} instance, but different instances
+ * can have different contexts; don't cache any calculations based on theme values globally.
+ */
+ public ThemedReactContext getThemedContext() {
+ return Assertions.assertNotNull(mThemedContext);
+ }
+
+ protected void setThemedContext(ThemedReactContext themedContext) {
+ mThemedContext = themedContext;
+ }
+
+ /* package */ void setShouldNotifyOnLayout(boolean shouldNotifyOnLayout) {
+ mShouldNotifyOnLayout = shouldNotifyOnLayout;
+ }
+
+ /* package */ boolean shouldNotifyOnLayout() {
+ return mShouldNotifyOnLayout;
+ }
+
+ /**
+ * Adds a child that the native view hierarchy will have at this index in the native view
+ * corresponding to this node.
+ */
+ public void addNativeChildAt(ReactShadowNode child, int nativeIndex) {
+ Assertions.assertCondition(!mIsLayoutOnly);
+ Assertions.assertCondition(!child.mIsLayoutOnly);
+
+ if (mNativeChildren == null) {
+ mNativeChildren = new ArrayList<>(4);
+ }
+
+ mNativeChildren.add(nativeIndex, child);
+ child.mNativeParent = this;
+ }
+
+ public ReactShadowNode removeNativeChildAt(int i) {
+ Assertions.assertNotNull(mNativeChildren);
+ ReactShadowNode removed = mNativeChildren.remove(i);
+ removed.mNativeParent = null;
+ return removed;
+ }
+
+ public int getNativeChildCount() {
+ return mNativeChildren == null ? 0 : mNativeChildren.size();
+ }
+
+ public int indexOfNativeChild(ReactShadowNode nativeChild) {
+ Assertions.assertNotNull(mNativeChildren);
+ return mNativeChildren.indexOf(nativeChild);
+ }
+
+ public @Nullable ReactShadowNode getNativeParent() {
+ return mNativeParent;
+ }
+
+ /**
+ * Sets whether this node only contributes to the layout of its children without doing any
+ * drawing or functionality itself.
+ */
+ public void setIsLayoutOnly(boolean isLayoutOnly) {
+ Assertions.assertCondition(getParent() == null, "Must remove from no opt parent first");
+ Assertions.assertCondition(mNativeParent == null, "Must remove from native parent first");
+ Assertions.assertCondition(getNativeChildCount() == 0, "Must remove all native children first");
+ mIsLayoutOnly = isLayoutOnly;
+ }
+
+ public boolean isLayoutOnly() {
+ return mIsLayoutOnly;
+ }
+
+ public int getTotalNativeChildren() {
+ return mTotalNativeChildren;
+ }
+
+ /**
+ * Returns the offset within the native children owned by all layout-only nodes in the subtree
+ * rooted at this node for the given child. Put another way, this returns the number of native
+ * nodes (nodes not optimized out of the native tree) that are a) to the left (visited before by a
+ * DFS) of the given child in the subtree rooted at this node and b) do not have a native parent
+ * in this subtree (which means that the given child will be a sibling of theirs in the final
+ * native hierarchy since they'll get attached to the same native parent).
+ *
+ * Basically, a view might have children that have been optimized away by
+ * {@link NativeViewHierarchyOptimizer}. Since those children will then add their native children
+ * to this view, we now have ranges of native children that correspond to single unoptimized
+ * children. The purpose of this method is to return the index within the native children that
+ * corresponds to the **start** of the native children that belong to the given child. Also, note
+ * that all of the children of a view might be optimized away, so this could return the same value
+ * for multiple different children.
+ *
+ * Example. Native children are represented by (N) where N is the no-opt child they came from. If
+ * no children are optimized away it'd look like this: (0) (1) (2) (3) ... (n)
+ *
+ * In case some children are optimized away, it might look like this:
+ * (0) (1) (1) (1) (3) (3) (4)
+ *
+ * In that case:
+ * getNativeOffsetForChild(Node 0) => 0
+ * getNativeOffsetForChild(Node 1) => 1
+ * getNativeOffsetForChild(Node 2) => 4
+ * getNativeOffsetForChild(Node 3) => 4
+ * getNativeOffsetForChild(Node 4) => 6
+ */
+ public int getNativeOffsetForChild(ReactShadowNode child) {
+ int index = 0;
+ boolean found = false;
+ for (int i = 0; i < getChildCount(); i++) {
+ ReactShadowNode current = getChildAt(i);
+ if (child == current) {
+ found = true;
+ break;
+ }
+ index += (current.mIsLayoutOnly ? current.getTotalNativeChildren() : 1);
+ }
+ if (!found) {
+ throw new RuntimeException("Child " + child.mReactTag + " was not a child of " + mReactTag);
+ }
+ return index;
+ }
+
+ /**
+ * @return the x position of the corresponding view on the screen, rounded to pixels
+ */
+ public int getScreenX() {
+ return Math.round(getLayoutX());
+ }
+
+ /**
+ * @return the y position of the corresponding view on the screen, rounded to pixels
+ */
+ public int getScreenY() {
+ return Math.round(getLayoutY());
+ }
+
+ /**
+ * @return width corrected for rounding to pixels.
+ */
+ public int getScreenWidth() {
+ return Math.round(mAbsoluteRight - mAbsoluteLeft);
+ }
+
+ /**
+ * @return height corrected for rounding to pixels.
+ */
+ public int getScreenHeight() {
+ return Math.round(mAbsoluteBottom - mAbsoluteTop);
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/RootView.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/RootView.java
new file mode 100644
index 00000000000000..05a11ee95535de
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/RootView.java
@@ -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.uimanager;
+
+import android.view.MotionEvent;
+
+/**
+ * Interface for the root native view of a React native application.
+ */
+public interface RootView {
+
+ /**
+ * Called when a child starts a native gesture (e.g. a scroll in a ScrollView). Should be called
+ * from the child's onTouchIntercepted implementation.
+ */
+ void onChildStartedNativeGesture(MotionEvent androidEvent);
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/RootViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/RootViewManager.java
new file mode 100644
index 00000000000000..b9ee413b4a577a
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/RootViewManager.java
@@ -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.uimanager;
+
+import android.view.ViewGroup;
+
+/**
+ * View manager for ReactRootView components.
+ */
+public class RootViewManager extends ViewGroupManager {
+
+ public static final String REACT_CLASS = "RootView";
+
+ @Override
+ public String getName() {
+ return REACT_CLASS;
+ }
+
+ @Override
+ protected ViewGroup createViewInstance(ThemedReactContext reactContext) {
+ return new SizeMonitoringFrameLayout(reactContext);
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/RootViewUtil.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/RootViewUtil.java
new file mode 100644
index 00000000000000..e12a76488a8894
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/RootViewUtil.java
@@ -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.uimanager;
+
+import android.view.View;
+import android.view.ViewParent;
+
+import com.facebook.infer.annotation.Assertions;
+
+public class RootViewUtil {
+
+ /**
+ * Returns the root view of a given view in a react application.
+ */
+ public static RootView getRootView(View reactView) {
+ View current = reactView;
+ while (true) {
+ if (current instanceof RootView) {
+ return (RootView) current;
+ }
+ ViewParent next = current.getParent();
+ Assertions.assertNotNull(next);
+ Assertions.assertCondition(next instanceof View);
+ current = (View) next;
+ }
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ShadowNodeRegistry.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ShadowNodeRegistry.java
new file mode 100644
index 00000000000000..9ffdcd7a433cd4
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ShadowNodeRegistry.java
@@ -0,0 +1,72 @@
+/**
+ * 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.uimanager;
+
+import android.util.SparseArray;
+import android.util.SparseBooleanArray;
+
+/**
+ * Simple container class to keep track of {@link ReactShadowNode}s associated with a particular
+ * UIManagerModule instance.
+ */
+/*package*/ class ShadowNodeRegistry {
+
+ private final SparseArray mTagsToCSSNodes;
+ private final SparseBooleanArray mRootTags;
+
+ public ShadowNodeRegistry() {
+ mTagsToCSSNodes = new SparseArray<>();
+ mRootTags = new SparseBooleanArray();
+ }
+
+ public void addRootNode(ReactShadowNode node) {
+ int tag = node.getReactTag();
+ mTagsToCSSNodes.put(tag, node);
+ mRootTags.put(tag, true);
+ }
+
+ public void removeRootNode(int tag) {
+ if (!mRootTags.get(tag)) {
+ throw new IllegalViewOperationException(
+ "View with tag " + tag + " is not registered as a root view");
+ }
+
+ mTagsToCSSNodes.remove(tag);
+ mRootTags.delete(tag);
+ }
+
+ public void addNode(ReactShadowNode node) {
+ mTagsToCSSNodes.put(node.getReactTag(), node);
+ }
+
+ public void removeNode(int tag) {
+ if (mRootTags.get(tag)) {
+ throw new IllegalViewOperationException(
+ "Trying to remove root node " + tag + " without using removeRootNode!");
+ }
+ mTagsToCSSNodes.remove(tag);
+ }
+
+ public ReactShadowNode getNode(int tag) {
+ return mTagsToCSSNodes.get(tag);
+ }
+
+ public boolean isRootNode(int tag) {
+ return mRootTags.get(tag);
+ }
+
+ public int getRootNodeCount() {
+ return mRootTags.size();
+ }
+
+ public int getRootTag(int index) {
+ return mRootTags.keyAt(index);
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/SimpleViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/SimpleViewManager.java
new file mode 100644
index 00000000000000..f338776e91eb6e
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/SimpleViewManager.java
@@ -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.uimanager;
+
+import android.view.View;
+
+/**
+ * A partial implementation of {@link ViewManager} that applies common properties such as background
+ * color, opacity and CSS layout. Implementations should make sure to call
+ * {@code super.updateView()} in order for these properties to be applied.
+ *
+ * @param the view handled by this manager
+ */
+public abstract class SimpleViewManager extends ViewManager {
+
+ @Override
+ public ReactShadowNode createCSSNodeInstance() {
+ return new ReactShadowNode();
+ }
+
+ @Override
+ public void updateView(T root, CatalystStylesDiffMap props) {
+ BaseViewPropertyApplicator.applyCommonViewProperties(root, props);
+ }
+
+ @Override
+ public void updateExtraData(T root, Object extraData) {
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/SizeMonitoringFrameLayout.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/SizeMonitoringFrameLayout.java
new file mode 100644
index 00000000000000..fbfd531c89c89a
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/SizeMonitoringFrameLayout.java
@@ -0,0 +1,56 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+package com.facebook.react.uimanager;
+
+import javax.annotation.Nullable;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.FrameLayout;
+
+/**
+ * Subclass of {@link FrameLayout} that allows registering for size change events. The main purpose
+ * for this class is to hide complexity of {@link ReactRootView} from the code under
+ * {@link com.facebook.react.uimanager} package.
+ */
+public class SizeMonitoringFrameLayout extends FrameLayout {
+
+ public static interface OnSizeChangedListener {
+ void onSizeChanged(int width, int height, int oldWidth, int oldHeight);
+ }
+
+ private @Nullable OnSizeChangedListener mOnSizeChangedListener;
+
+ public SizeMonitoringFrameLayout(Context context) {
+ super(context);
+ }
+
+ public SizeMonitoringFrameLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public SizeMonitoringFrameLayout(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ public void setOnSizeChangedListener(OnSizeChangedListener onSizeChangedListener) {
+ mOnSizeChangedListener = onSizeChangedListener;
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+
+ if (mOnSizeChangedListener != null) {
+ mOnSizeChangedListener.onSizeChanged(w, h, oldw, oldh);
+ }
+ }
+
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ThemedReactContext.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ThemedReactContext.java
new file mode 100644
index 00000000000000..f3511bd2879039
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ThemedReactContext.java
@@ -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.uimanager;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.os.Bundle;
+
+import com.facebook.react.bridge.ReactApplicationContext;
+import com.facebook.react.bridge.ReactContext;
+import com.facebook.react.bridge.LifecycleEventListener;
+
+//
+
+/**
+ * Wraps {@link ReactContext} with the base {@link Context} passed into the constructor.
+ * It provides also a way to start activities using the viewContext to which RN native views belong.
+ * It delegates lifecycle listener registration to the original instance of {@link ReactContext}
+ * which is supposed to receive the lifecycle events. At the same time we disallow receiving
+ * lifecycle events for this wrapper instances.
+ * TODO: T7538544 Rename ThemedReactContext to be in alignment with name of ReactApplicationContext
+ */
+public class ThemedReactContext extends ReactContext {
+
+ private final ReactApplicationContext mReactApplicationContext;
+
+ public ThemedReactContext(ReactApplicationContext reactApplicationContext, Context base) {
+ super(base);
+ initializeWithInstance(reactApplicationContext.getCatalystInstance());
+ mReactApplicationContext = reactApplicationContext;
+ }
+
+ @Override
+ public void addLifecycleEventListener(LifecycleEventListener listener) {
+ mReactApplicationContext.addLifecycleEventListener(listener);
+ }
+
+ @Override
+ public void removeLifecycleEventListener(LifecycleEventListener listener) {
+ mReactApplicationContext.removeLifecycleEventListener(listener);
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/TouchTargetHelper.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/TouchTargetHelper.java
new file mode 100644
index 00000000000000..0902c8dd9a70fd
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/TouchTargetHelper.java
@@ -0,0 +1,146 @@
+/**
+ * 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.uimanager;
+
+import javax.annotation.Nullable;
+
+import android.graphics.Rect;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
+import com.facebook.react.bridge.UiThreadUtil;
+
+/**
+ * Class responsible for identifying which react view should handle a given {@link MotionEvent}.
+ * It uses the event coordinates to traverse the view hierarchy and return a suitable view.
+ */
+public class TouchTargetHelper {
+
+ private static final Rect mVisibleRect = new Rect();
+ private static final int[] mViewLocationInScreen = {0, 0};
+
+ /**
+ * Find touch event target view within the provided container given the coordinates provided
+ * via {@link MotionEvent}.
+ *
+ * @param eventY the Y screen coordinate of the touch location
+ * @param eventX the X screen coordinate of the touch location
+ * @param viewGroup the container view to traverse
+ * @return the react tag ID of the child view that should handle the event
+ */
+ public static int findTargetTagForTouch(
+ float eventY,
+ float eventX,
+ ViewGroup viewGroup) {
+ UiThreadUtil.assertOnUiThread();
+ int targetTag = viewGroup.getId();
+ View nativeTargetView = findTouchTargetView(eventX, eventY, viewGroup);
+ if (nativeTargetView != null) {
+ View reactTargetView = findClosestReactAncestor(nativeTargetView);
+ if (reactTargetView != null) {
+ targetTag = getTouchTargetForView(reactTargetView, eventX, eventY);
+ }
+ }
+ return targetTag;
+ }
+
+ private static View findClosestReactAncestor(View view) {
+ while (view != null && view.getId() <= 0) {
+ view = (View) view.getParent();
+ }
+ return view;
+ }
+
+ /**
+ * Returns the touch target View that is either viewGroup or one if its descendants.
+ * This is a recursive DFS since view the entire tree must be parsed until the target is found.
+ * If the search does not backtrack, it is possible to follow a branch that cannot be a target
+ * (because of pointerEvents). For example, if both C and E can be the target of an event:
+ * A (pointerEvents: auto) - B (pointerEvents: box-none) - C (pointerEvents: none)
+ * \ D (pointerEvents: auto) - E (pointerEvents: auto)
+ * If the search goes down the first branch, it would return A as the target, which is incorrect.
+ * NB: This method is not thread-safe as it uses static instance of {@link Rect}
+ */
+ private static View findTouchTargetView(float eventX, float eventY, ViewGroup viewGroup) {
+ int childrenCount = viewGroup.getChildCount();
+ for (int i = childrenCount - 1; i >= 0; i--) {
+ View child = viewGroup.getChildAt(i);
+ // Views with `removeClippedSubviews` are exposing removed subviews through `getChildAt` to
+ // support proper view cleanup. Views removed by this option will be detached from it's
+ // parent, therefore `getGlobalVisibleRect` call will return bogus result as it treat view
+ // with no parent as a root of the view hierarchy. To prevent this from happening we check
+ // that view has a parent before visiting it.
+ if (child.getParent() != null && child.getGlobalVisibleRect(mVisibleRect)) {
+ if (eventX >= mVisibleRect.left && eventX <= mVisibleRect.right
+ && eventY >= mVisibleRect.top && eventY <= mVisibleRect.bottom) {
+ View targetView = findTouchTargetViewWithPointerEvents(eventX, eventY, child);
+ if (targetView != null) {
+ return targetView;
+ }
+ }
+ }
+ }
+ return viewGroup;
+ }
+
+ /**
+ * Returns the touch target View of the event given, or null if neither the given View nor any of
+ * its descendants are the touch target.
+ */
+ private static @Nullable View findTouchTargetViewWithPointerEvents(
+ float eventX,
+ float eventY,
+ View view) {
+ PointerEvents pointerEvents = view instanceof ReactPointerEventsView ?
+ ((ReactPointerEventsView) view).getPointerEvents() : PointerEvents.AUTO;
+ if (pointerEvents == PointerEvents.NONE) {
+ // This view and its children can't be the target
+ return null;
+
+ } else if (pointerEvents == PointerEvents.BOX_ONLY) {
+ // This view is the target, its children don't matter
+ return view;
+
+ } else if (pointerEvents == PointerEvents.BOX_NONE) {
+ // This view can't be the target, but its children might
+ if (view instanceof ViewGroup) {
+ View targetView = findTouchTargetView(eventX, eventY, (ViewGroup) view);
+ return targetView != view ? targetView : null;
+ }
+ return null;
+
+ } else if (pointerEvents == PointerEvents.AUTO) {
+ // Either this view or one of its children is the target
+ if (view instanceof ViewGroup) {
+ return findTouchTargetView(eventX, eventY, (ViewGroup) view);
+ }
+ return view;
+
+ } else {
+ throw new JSApplicationIllegalArgumentException(
+ "Unknown pointer event type: " + pointerEvents.toString());
+ }
+ }
+
+ private static int getTouchTargetForView(View targetView, float eventX, float eventY) {
+ if (targetView instanceof ReactCompoundView) {
+ // Use coordinates relative to the view. Use getLocationOnScreen() API, which is slightly more
+ // expensive than getGlobalVisibleRect(), otherwise partially visible views offset is wrong.
+ targetView.getLocationOnScreen(mViewLocationInScreen);
+ return ((ReactCompoundView) targetView).reactTagForTouch(
+ eventX - mViewLocationInScreen[0],
+ eventY - mViewLocationInScreen[1]);
+ }
+ return targetView.getId();
+ }
+
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java
new file mode 100644
index 00000000000000..25b97a863973e9
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java
@@ -0,0 +1,837 @@
+/**
+ * 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.uimanager;
+
+import javax.annotation.Nullable;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+
+import android.util.DisplayMetrics;
+
+import com.facebook.csslayout.CSSLayoutContext;
+import com.facebook.react.animation.Animation;
+import com.facebook.react.animation.AnimationRegistry;
+import com.facebook.react.bridge.ReactApplicationContext;
+import com.facebook.react.uimanager.debug.NotThreadSafeUiManagerDebugListener;
+import com.facebook.react.uimanager.events.EventDispatcher;
+import com.facebook.infer.annotation.Assertions;
+import com.facebook.react.bridge.Arguments;
+import com.facebook.react.bridge.Callback;
+import com.facebook.react.bridge.LifecycleEventListener;
+import com.facebook.react.bridge.OnBatchCompleteListener;
+import com.facebook.react.bridge.ReactContextBaseJavaModule;
+import com.facebook.react.bridge.ReactMethod;
+import com.facebook.react.bridge.ReadableArray;
+import com.facebook.react.bridge.ReadableMap;
+import com.facebook.react.bridge.SoftAssertions;
+import com.facebook.react.bridge.UiThreadUtil;
+import com.facebook.react.bridge.WritableArray;
+import com.facebook.systrace.Systrace;
+import com.facebook.systrace.SystraceMessage;
+
+/**
+ * Native module to allow JS to create and update native Views.
+ *
+ *
+ *
== Transactional Requirement ==
+ * A requirement of this class is to make sure that transactional UI updates occur all at, meaning
+ * that no intermediate state is ever rendered to the screen. For example, if a JS application
+ * update changes the background of View A to blue and the width of View B to 100, both need to
+ * appear at once. Practically, this means that all UI update code related to a single transaction
+ * must be executed as a single code block on the UI thread. Executing as multiple code blocks
+ * could allow the platform UI system to interrupt and render a partial UI state.
+ *
+ *
+ * To facilitate this, this module enqueues operations that are then applied to native view
+ * hierarchy through {@link NativeViewHierarchyManager} at the end of each transaction.
+ *
+ *
+ *
== CSSNodes ==
+ * In order to allow layout and measurement to occur on a non-UI thread, this module also
+ * operates on intermediate CSSNode objects that correspond to a native view. These CSSNode are able
+ * to calculate layout according to their styling rules, and then the resulting x/y/width/height of
+ * that layout is scheduled as an operation that will be applied to native view hierarchy at the end
+ * of current batch.
+ *
+ *
+ * TODO(5241856): Investigate memory usage of creating many small objects in UIManageModule and
+ * consider implementing a pool
+ * TODO(5483063): Don't dispatch the view hierarchy at the end of a batch if no UI changes occurred
+ */
+public class UIManagerModule extends ReactContextBaseJavaModule implements
+ OnBatchCompleteListener, LifecycleEventListener {
+
+ // Keep in sync with ReactIOSTagHandles JS module - see that file for an explanation on why the
+ // increment here is 10
+ private static final int ROOT_VIEW_TAG_INCREMENT = 10;
+
+ private final NativeViewHierarchyManager mNativeViewHierarchyManager;
+ private final EventDispatcher mEventDispatcher;
+ private final AnimationRegistry mAnimationRegistry = new AnimationRegistry();
+ private final ShadowNodeRegistry mShadowNodeRegistry = new ShadowNodeRegistry();
+ private final ViewManagerRegistry mViewManagers;
+ private final CSSLayoutContext mLayoutContext = new CSSLayoutContext();
+ private final Map mModuleConstants;
+ private final UIViewOperationQueue mOperationsQueue;
+ private final NativeViewHierarchyOptimizer mNativeViewHierarchyOptimizer;
+ private final int[] mMeasureBuffer = new int[4];
+
+ private @Nullable NotThreadSafeUiManagerDebugListener mUiManagerDebugListener;
+ private int mNextRootViewTag = 1;
+ private int mBatchId = 0;
+
+ public UIManagerModule(ReactApplicationContext reactContext, List viewManagerList) {
+ super(reactContext);
+ mViewManagers = new ViewManagerRegistry(viewManagerList);
+ mEventDispatcher = new EventDispatcher(reactContext);
+ mNativeViewHierarchyManager = new NativeViewHierarchyManager(
+ mAnimationRegistry,
+ mViewManagers);
+ mOperationsQueue = new UIViewOperationQueue(
+ reactContext,
+ this,
+ mNativeViewHierarchyManager,
+ mAnimationRegistry);
+ mNativeViewHierarchyOptimizer = new NativeViewHierarchyOptimizer(
+ mOperationsQueue,
+ mShadowNodeRegistry);
+ DisplayMetrics displayMetrics = reactContext.getResources().getDisplayMetrics();
+ DisplayMetricsHolder.setDisplayMetrics(displayMetrics);
+
+ mModuleConstants = UIManagerModuleConstantsHelper.createConstants(
+ displayMetrics,
+ viewManagerList);
+ reactContext.addLifecycleEventListener(this);
+ }
+
+ @Override
+ public String getName() {
+ return "RKUIManager";
+ }
+
+ @Override
+ public Map getConstants() {
+ return mModuleConstants;
+ }
+
+ @Override
+ public void onHostResume() {
+ mOperationsQueue.resumeFrameCallback();
+ }
+
+ @Override
+ public void onHostPause() {
+ mOperationsQueue.pauseFrameCallback();
+ }
+
+ @Override
+ public void onHostDestroy() {
+ }
+
+ @Override
+ public void onCatalystInstanceDestroy() {
+ super.onCatalystInstanceDestroy();
+ mEventDispatcher.onCatalystInstanceDestroyed();
+ }
+
+ /**
+ * Registers a new root view. JS can use the returned tag with manageChildren to add/remove
+ * children to this view.
+ *
+ * Note that this must be called after getWidth()/getHeight() actually return something. See
+ * CatalystApplicationFragment as an example.
+ *
+ * TODO(6242243): Make addMeasuredRootView thread safe
+ * NB: this method is horribly not-thread-safe, the only reason it works right now is because
+ * it's called exactly once and is called before any JS calls are made. As soon as that fact no
+ * longer holds, this method will need to be fixed.
+ */
+ public int addMeasuredRootView(final SizeMonitoringFrameLayout rootView) {
+ final int tag = mNextRootViewTag;
+ mNextRootViewTag += ROOT_VIEW_TAG_INCREMENT;
+
+ final ReactShadowNode rootCSSNode = new ReactShadowNode();
+ rootCSSNode.setReactTag(tag);
+ final ThemedReactContext themedRootContext =
+ new ThemedReactContext(getReactApplicationContext(), rootView.getContext());
+ rootCSSNode.setThemedContext(themedRootContext);
+ // If LayoutParams sets size explicitly, we can use that. Otherwise get the size from the view.
+ if (rootView.getLayoutParams() != null &&
+ rootView.getLayoutParams().width > 0 &&
+ rootView.getLayoutParams().height > 0) {
+ rootCSSNode.setStyleWidth(rootView.getLayoutParams().width);
+ rootCSSNode.setStyleHeight(rootView.getLayoutParams().height);
+ } else {
+ rootCSSNode.setStyleWidth(rootView.getWidth());
+ rootCSSNode.setStyleHeight(rootView.getHeight());
+ }
+ rootCSSNode.setViewClassName("Root");
+
+ rootView.setOnSizeChangedListener(
+ new SizeMonitoringFrameLayout.OnSizeChangedListener() {
+ @Override
+ public void onSizeChanged(final int width, final int height, int oldW, int oldH) {
+ getReactApplicationContext().runOnNativeModulesQueueThread(
+ new Runnable() {
+ @Override
+ public void run() {
+ updateRootNodeSize(rootCSSNode, width, height);
+ }
+ });
+ }
+ });
+
+ mShadowNodeRegistry.addRootNode(rootCSSNode);
+
+ if (UiThreadUtil.isOnUiThread()) {
+ mNativeViewHierarchyManager.addRootView(tag, rootView, themedRootContext);
+ } else {
+ final Semaphore semaphore = new Semaphore(0);
+ getReactApplicationContext().runOnUiQueueThread(
+ new Runnable() {
+ @Override
+ public void run() {
+ mNativeViewHierarchyManager.addRootView(tag, rootView, themedRootContext);
+ semaphore.release();
+ }
+ });
+ try {
+ SoftAssertions.assertCondition(
+ semaphore.tryAcquire(5000, TimeUnit.MILLISECONDS),
+ "Timed out adding root view");
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ return tag;
+ }
+
+ @ReactMethod
+ public void removeRootView(int rootViewTag) {
+ mShadowNodeRegistry.removeRootNode(rootViewTag);
+ mOperationsQueue.enqueueRemoveRootView(rootViewTag);
+ }
+
+ private void updateRootNodeSize(ReactShadowNode rootCSSNode, int newWidth, int newHeight) {
+ getReactApplicationContext().assertOnNativeModulesQueueThread();
+
+ rootCSSNode.setStyleWidth(newWidth);
+ rootCSSNode.setStyleHeight(newHeight);
+
+ // If we're in the middle of a batch, the change will automatically be dispatched at the end of
+ // the batch. As all batches are executed as a single runnable on the event queue this should
+ // always be empty, but that calling architecture is an implementation detail.
+ if (mOperationsQueue.isEmpty()) {
+ dispatchViewUpdates(-1); // -1 = no associated batch id
+ }
+ }
+
+ @ReactMethod
+ public void createView(int tag, String className, int rootViewTag, ReadableMap props) {
+ ViewManager viewManager = mViewManagers.get(className);
+ ReactShadowNode cssNode = viewManager.createCSSNodeInstance();
+ ReactShadowNode rootNode = mShadowNodeRegistry.getNode(rootViewTag);
+ cssNode.setReactTag(tag);
+ cssNode.setViewClassName(className);
+ cssNode.setRootNode(rootNode);
+ cssNode.setThemedContext(rootNode.getThemedContext());
+
+ mShadowNodeRegistry.addNode(cssNode);
+
+ CatalystStylesDiffMap styles = null;
+ if (props != null) {
+ styles = new CatalystStylesDiffMap(props);
+ cssNode.updateProperties(styles);
+ }
+
+ if (!cssNode.isVirtual()) {
+ mNativeViewHierarchyOptimizer.handleCreateView(cssNode, rootViewTag, styles);
+ }
+ }
+
+ @ReactMethod
+ public void updateView(int tag, String className, ReadableMap props) {
+ ViewManager viewManager = mViewManagers.get(className);
+ if (viewManager == null) {
+ throw new IllegalViewOperationException("Got unknown view type: " + className);
+ }
+ ReactShadowNode cssNode = mShadowNodeRegistry.getNode(tag);
+ if (cssNode == null) {
+ throw new IllegalViewOperationException("Trying to update non-existent view with tag " + tag);
+ }
+
+ if (props != null) {
+ CatalystStylesDiffMap styles = new CatalystStylesDiffMap(props);
+ cssNode.updateProperties(styles);
+ if (!cssNode.isVirtual()) {
+ mNativeViewHierarchyOptimizer.handleUpdateView(cssNode, className, styles);
+ }
+ }
+ }
+
+ /**
+ * Interface for adding/removing/moving views within a parent view from JS.
+ *
+ * @param viewTag the view tag of the parent view
+ * @param moveFrom a list of indices in the parent view to move views from
+ * @param moveTo parallel to moveFrom, a list of indices in the parent view to move views to
+ * @param addChildTags a list of tags of views to add to the parent
+ * @param addAtIndices parallel to addChildTags, a list of indices to insert those children at
+ * @param removeFrom a list of indices of views to permanently remove. The memory for the
+ * corresponding views and data structures should be reclaimed.
+ */
+ @ReactMethod
+ public void manageChildren(
+ int viewTag,
+ @Nullable ReadableArray moveFrom,
+ @Nullable ReadableArray moveTo,
+ @Nullable ReadableArray addChildTags,
+ @Nullable ReadableArray addAtIndices,
+ @Nullable ReadableArray removeFrom) {
+ ReactShadowNode cssNodeToManage = mShadowNodeRegistry.getNode(viewTag);
+
+ int numToMove = moveFrom == null ? 0 : moveFrom.size();
+ int numToAdd = addChildTags == null ? 0 : addChildTags.size();
+ int numToRemove = removeFrom == null ? 0 : removeFrom.size();
+
+ if (numToMove != 0 && (moveTo == null || numToMove != moveTo.size())) {
+ throw new IllegalViewOperationException("Size of moveFrom != size of moveTo!");
+ }
+
+ if (numToAdd != 0 && (addAtIndices == null || numToAdd != addAtIndices.size())) {
+ throw new IllegalViewOperationException("Size of addChildTags != size of addAtIndices!");
+ }
+
+ // We treat moves as an add and a delete
+ ViewAtIndex[] viewsToAdd = new ViewAtIndex[numToMove + numToAdd];
+ int[] indicesToRemove = new int[numToMove + numToRemove];
+ int[] tagsToRemove = new int[indicesToRemove.length];
+ int[] tagsToDelete = new int[numToRemove];
+
+ if (numToMove > 0) {
+ Assertions.assertNotNull(moveFrom);
+ Assertions.assertNotNull(moveTo);
+ for (int i = 0; i < numToMove; i++) {
+ int moveFromIndex = moveFrom.getInt(i);
+ int tagToMove = cssNodeToManage.getChildAt(moveFromIndex).getReactTag();
+ viewsToAdd[i] = new ViewAtIndex(
+ tagToMove,
+ moveTo.getInt(i));
+ indicesToRemove[i] = moveFromIndex;
+ tagsToRemove[i] = tagToMove;
+ }
+ }
+
+ if (numToAdd > 0) {
+ Assertions.assertNotNull(addChildTags);
+ Assertions.assertNotNull(addAtIndices);
+ for (int i = 0; i < numToAdd; i++) {
+ int viewTagToAdd = addChildTags.getInt(i);
+ int indexToAddAt = addAtIndices.getInt(i);
+ viewsToAdd[numToMove + i] = new ViewAtIndex(viewTagToAdd, indexToAddAt);
+ }
+ }
+
+ if (numToRemove > 0) {
+ Assertions.assertNotNull(removeFrom);
+ for (int i = 0; i < numToRemove; i++) {
+ int indexToRemove = removeFrom.getInt(i);
+ int tagToRemove = cssNodeToManage.getChildAt(indexToRemove).getReactTag();
+ indicesToRemove[numToMove + i] = indexToRemove;
+ tagsToRemove[numToMove + i] = tagToRemove;
+ tagsToDelete[i] = tagToRemove;
+ }
+ }
+
+ // NB: moveFrom and removeFrom are both relative to the starting state of the View's children.
+ // moveTo and addAt are both relative to the final state of the View's children.
+ //
+ // 1) Sort the views to add and indices to remove by index
+ // 2) Iterate the indices being removed from high to low and remove them. Going high to low
+ // makes sure we remove the correct index when there are multiple to remove.
+ // 3) Iterate the views being added by index low to high and add them. Like the view removal,
+ // iteration direction is important to preserve the correct index.
+
+ Arrays.sort(viewsToAdd, ViewAtIndex.COMPARATOR);
+ Arrays.sort(indicesToRemove);
+
+ // Apply changes to CSSNode hierarchy
+ int lastIndexRemoved = -1;
+ for (int i = indicesToRemove.length - 1; i >= 0; i--) {
+ int indexToRemove = indicesToRemove[i];
+ if (indexToRemove == lastIndexRemoved) {
+ throw new IllegalViewOperationException("Repeated indices in Removal list for view tag: "
+ + viewTag);
+ }
+ cssNodeToManage.removeChildAt(indicesToRemove[i]);
+ lastIndexRemoved = indicesToRemove[i];
+ }
+
+ for (int i = 0; i < viewsToAdd.length; i++) {
+ ViewAtIndex viewAtIndex = viewsToAdd[i];
+ ReactShadowNode cssNodeToAdd = mShadowNodeRegistry.getNode(viewAtIndex.mTag);
+ if (cssNodeToAdd == null) {
+ throw new IllegalViewOperationException("Trying to add unknown view tag: "
+ + viewAtIndex.mTag);
+ }
+ cssNodeToManage.addChildAt(cssNodeToAdd, viewAtIndex.mIndex);
+ }
+
+ if (!cssNodeToManage.isVirtual() && !cssNodeToManage.isVirtualAnchor()) {
+ mNativeViewHierarchyOptimizer.handleManageChildren(
+ cssNodeToManage,
+ indicesToRemove,
+ tagsToRemove,
+ viewsToAdd,
+ tagsToDelete);
+ }
+
+ for (int i = 0; i < tagsToDelete.length; i++) {
+ removeCSSNode(tagsToDelete[i]);
+ }
+ }
+
+ private void removeCSSNode(int tag) {
+ ReactShadowNode node = mShadowNodeRegistry.getNode(tag);
+ mShadowNodeRegistry.removeNode(tag);
+ for (int i = 0;i < node.getChildCount(); i++) {
+ removeCSSNode(node.getChildAt(i).getReactTag());
+ }
+ }
+
+ /**
+ * Replaces the View specified by oldTag with the View specified by newTag within oldTag's parent.
+ * This resolves to a simple {@link #manageChildren} call, but React doesn't have enough info in
+ * JS to formulate it itself.
+ */
+ @ReactMethod
+ public void replaceExistingNonRootView(int oldTag, int newTag) {
+ if (mShadowNodeRegistry.isRootNode(oldTag) || mShadowNodeRegistry.isRootNode(newTag)) {
+ throw new IllegalViewOperationException("Trying to add or replace a root tag!");
+ }
+
+ ReactShadowNode oldNode = mShadowNodeRegistry.getNode(oldTag);
+ if (oldNode == null) {
+ throw new IllegalViewOperationException("Trying to replace unknown view tag: " + oldTag);
+ }
+
+ ReactShadowNode parent = oldNode.getParent();
+ if (parent == null) {
+ throw new IllegalViewOperationException("Node is not attached to a parent: " + oldTag);
+ }
+
+ int oldIndex = parent.indexOf(oldNode);
+ if (oldIndex < 0) {
+ throw new IllegalStateException("Didn't find child tag in parent");
+ }
+
+ WritableArray tagsToAdd = Arguments.createArray();
+ tagsToAdd.pushInt(newTag);
+
+ WritableArray addAtIndices = Arguments.createArray();
+ addAtIndices.pushInt(oldIndex);
+
+ WritableArray indicesToRemove = Arguments.createArray();
+ indicesToRemove.pushInt(oldIndex);
+
+ manageChildren(parent.getReactTag(), null, null, tagsToAdd, addAtIndices, indicesToRemove);
+ }
+
+ /**
+ * Method which takes a container tag and then releases all subviews for that container upon
+ * receipt.
+ * TODO: The method name is incorrect and will be renamed, #6033872
+ * @param containerTag the tag of the container for which the subviews must be removed
+ */
+ @ReactMethod
+ public void removeSubviewsFromContainerWithID(int containerTag) {
+ ReactShadowNode containerNode = mShadowNodeRegistry.getNode(containerTag);
+ if (containerNode == null) {
+ throw new IllegalViewOperationException(
+ "Trying to remove subviews of an unknown view tag: " + containerTag);
+ }
+
+ WritableArray indicesToRemove = Arguments.createArray();
+ for (int childIndex = 0; childIndex < containerNode.getChildCount(); childIndex++) {
+ indicesToRemove.pushInt(childIndex);
+ }
+
+ manageChildren(containerTag, null, null, null, null, indicesToRemove);
+ }
+
+ /**
+ * Determines the location on screen, width, and height of the given view and returns the values
+ * via an async callback.
+ */
+ @ReactMethod
+ public void measure(final int reactTag, final Callback callback) {
+ // This method is called by the implementation of JS touchable interface (see Touchable.js for
+ // more details) at the moment of touch activation. That is after user starts the gesture from
+ // a touchable view with a given reactTag, or when user drag finger back into the press
+ // activation area of a touchable view that have been activated before.
+ mOperationsQueue.enqueueMeasure(reactTag, callback);
+ }
+
+ /**
+ * Measures the view specified by tag relative to the given ancestorTag. This means that the
+ * returned x, y are relative to the origin x, y of the ancestor view. Results are stored in the
+ * given outputBuffer. We allow ancestor view and measured view to be the same, in which case
+ * the position always will be (0, 0) and method will only measure the view dimensions.
+ *
+ * NB: Unlike {@link #measure}, this will measure relative to the view layout, not the visible
+ * window which can cause unexpected results when measuring relative to things like ScrollViews
+ * that can have offset content on the screen.
+ */
+ @ReactMethod
+ public void measureLayout(
+ int tag,
+ int ancestorTag,
+ Callback errorCallback,
+ Callback successCallback) {
+ try {
+ measureLayout(tag, ancestorTag, mMeasureBuffer);
+ float relativeX = PixelUtil.toDIPFromPixel(mMeasureBuffer[0]);
+ float relativeY = PixelUtil.toDIPFromPixel(mMeasureBuffer[1]);
+ float width = PixelUtil.toDIPFromPixel(mMeasureBuffer[2]);
+ float height = PixelUtil.toDIPFromPixel(mMeasureBuffer[3]);
+ successCallback.invoke(relativeX, relativeY, width, height);
+ } catch (IllegalViewOperationException e) {
+ errorCallback.invoke(e.getMessage());
+ }
+ }
+
+ /**
+ * Like {@link #measure} and {@link #measureLayout} but measures relative to the immediate parent.
+ *
+ * NB: Unlike {@link #measure}, this will measure relative to the view layout, not the visible
+ * window which can cause unexpected results when measuring relative to things like ScrollViews
+ * that can have offset content on the screen.
+ */
+ @ReactMethod
+ public void measureLayoutRelativeToParent(
+ int tag,
+ Callback errorCallback,
+ Callback successCallback) {
+ try {
+ measureLayoutRelativeToParent(tag, mMeasureBuffer);
+ float relativeX = PixelUtil.toDIPFromPixel(mMeasureBuffer[0]);
+ float relativeY = PixelUtil.toDIPFromPixel(mMeasureBuffer[1]);
+ float width = PixelUtil.toDIPFromPixel(mMeasureBuffer[2]);
+ float height = PixelUtil.toDIPFromPixel(mMeasureBuffer[3]);
+ successCallback.invoke(relativeX, relativeY, width, height);
+ } catch (IllegalViewOperationException e) {
+ errorCallback.invoke(e.getMessage());
+ }
+ }
+
+ private void measureLayout(int tag, int ancestorTag, int[] outputBuffer) {
+ ReactShadowNode node = mShadowNodeRegistry.getNode(tag);
+ ReactShadowNode ancestor = mShadowNodeRegistry.getNode(ancestorTag);
+ if (node == null || ancestor == null) {
+ throw new IllegalViewOperationException(
+ "Tag " + (node == null ? tag : ancestorTag) + " does not exist");
+ }
+
+ if (node != ancestor) {
+ ReactShadowNode currentParent = node.getParent();
+ while (currentParent != ancestor) {
+ if (currentParent == null) {
+ throw new IllegalViewOperationException(
+ "Tag " + ancestorTag + " is not an ancestor of tag " + tag);
+ }
+ currentParent = currentParent.getParent();
+ }
+ }
+
+ measureLayoutRelativeToVerifiedAncestor(node, ancestor, outputBuffer);
+ }
+
+ private void measureLayoutRelativeToParent(int tag, int[] outputBuffer) {
+ ReactShadowNode node = mShadowNodeRegistry.getNode(tag);
+ if (node == null) {
+ throw new IllegalViewOperationException("No native view for tag " + tag + " exists!");
+ }
+ ReactShadowNode parent = node.getParent();
+ if (parent == null) {
+ throw new IllegalViewOperationException("View with tag " + tag + " doesn't have a parent!");
+ }
+
+ measureLayoutRelativeToVerifiedAncestor(node, parent, outputBuffer);
+ }
+
+ private void measureLayoutRelativeToVerifiedAncestor(
+ ReactShadowNode node,
+ ReactShadowNode ancestor,
+ int[] outputBuffer) {
+ int offsetX = 0;
+ int offsetY = 0;
+ if (node != ancestor) {
+ offsetX = Math.round(node.getLayoutX());
+ offsetY = Math.round(node.getLayoutY());
+ ReactShadowNode current = node.getParent();
+ while (current != ancestor) {
+ Assertions.assertNotNull(current);
+ assertNodeDoesNotNeedCustomLayoutForChildren(current);
+ offsetX += Math.round(current.getLayoutX());
+ offsetY += Math.round(current.getLayoutY());
+ current = current.getParent();
+ }
+ assertNodeDoesNotNeedCustomLayoutForChildren(ancestor);
+ }
+
+ outputBuffer[0] = offsetX;
+ outputBuffer[1] = offsetY;
+ outputBuffer[2] = node.getScreenWidth();
+ outputBuffer[3] = node.getScreenHeight();
+ }
+
+ private void assertNodeDoesNotNeedCustomLayoutForChildren(ReactShadowNode node) {
+ ViewManager viewManager = Assertions.assertNotNull(mViewManagers.get(node.getViewClass()));
+ ViewGroupManager viewGroupManager;
+ if (viewManager instanceof ViewGroupManager) {
+ viewGroupManager = (ViewGroupManager) viewManager;
+ } else {
+ throw new IllegalViewOperationException("Trying to use view " + node.getViewClass() +
+ " as a parent, but its Manager doesn't extends ViewGroupManager");
+ }
+ if (viewGroupManager != null && viewGroupManager.needsCustomLayoutForChildren()) {
+ throw new IllegalViewOperationException(
+ "Trying to measure a view using measureLayout/measureLayoutRelativeToParent relative to" +
+ " an ancestor that requires custom layout for it's children (" + node.getViewClass() +
+ "). Use measure instead.");
+ }
+ }
+
+ /**
+ * Find the touch target child native view in the supplied root view hierarchy, given a react
+ * target location.
+ *
+ * This method is currently used only by Element Inspector DevTool.
+ *
+ * @param reactTag the tag of the root view to traverse
+ * @param point an array containing both X and Y target location
+ * @param callback will be called if with the identified child view react ID, and measurement
+ * info. If no view was found, callback will be invoked with no data.
+ */
+ @ReactMethod
+ public void findSubviewIn(
+ final int reactTag,
+ final ReadableArray point,
+ final Callback callback) {
+ mOperationsQueue.enqueueFindTargetForTouch(
+ reactTag,
+ point.getInt(0),
+ point.getInt(1),
+ callback);
+ }
+
+ /**
+ * Registers a new Animation that can then be added to a View using {@link #addAnimation}.
+ */
+ public void registerAnimation(Animation animation) {
+ mOperationsQueue.enqueueRegisterAnimation(animation);
+ }
+
+ /**
+ * Adds an Animation previously registered with {@link #registerAnimation} to a View and starts it
+ */
+ public void addAnimation(final int reactTag, final int animationID, final Callback onSuccess) {
+ assertViewExists(reactTag, "addAnimation");
+ mOperationsQueue.enqueueAddAnimation(reactTag, animationID, onSuccess);
+ }
+
+ /**
+ * Removes an existing Animation, canceling it if it was in progress.
+ */
+ public void removeAnimation(int reactTag, int animationID) {
+ assertViewExists(reactTag, "removeAnimation");
+ mOperationsQueue.enqueueRemoveAnimation(animationID);
+ }
+
+ @ReactMethod
+ public void setJSResponder(int reactTag, boolean blockNativeResponder) {
+ assertViewExists(reactTag, "setJSResponder");
+ mOperationsQueue.enqueueSetJSResponder(reactTag, blockNativeResponder);
+ }
+
+ @ReactMethod
+ public void clearJSResponder() {
+ mOperationsQueue.enqueueClearJSResponder();
+ }
+
+ @ReactMethod
+ public void dispatchViewManagerCommand(
+ int reactTag,
+ int commandId,
+ ReadableArray commandArgs) {
+ assertViewExists(reactTag, "dispatchViewManagerCommand");
+ mOperationsQueue.enqueueDispatchCommand(reactTag, commandId, commandArgs);
+ }
+
+ /**
+ * Show a PopupMenu.
+ *
+ * @param reactTag the tag of the anchor view (the PopupMenu is displayed next to this view); this
+ * needs to be the tag of a native view (shadow views can not be anchors)
+ * @param items the menu items as an array of strings
+ * @param error will be called if there is an error displaying the menu
+ * @param success will be called with the position of the selected item as the first argument, or
+ * no arguments if the menu is dismissed
+ */
+ @ReactMethod
+ public void showPopupMenu(
+ int reactTag,
+ ReadableArray items,
+ Callback error,
+ Callback success) {
+ assertViewExists(reactTag, "showPopupMenu");
+ mOperationsQueue.enqueueShowPopupMenu(reactTag, items, error, success);
+ }
+
+ @ReactMethod
+ public void setMainScrollViewTag(int reactTag) {
+ // TODO(6588266): Implement if required
+ }
+
+ @ReactMethod
+ public void configureNextLayoutAnimation(
+ ReadableMap config,
+ Callback successCallback,
+ Callback errorCallback) {
+ // TODO(6588266): Implement if required
+ }
+
+ private void assertViewExists(int reactTag, String operationNameForExceptionMessage) {
+ if (mShadowNodeRegistry.getNode(reactTag) == null) {
+ throw new IllegalViewOperationException(
+ "Unable to execute operation " + operationNameForExceptionMessage + " on view with " +
+ "tag: " + reactTag + ", since the view does not exists");
+ }
+ }
+
+ /**
+ * To implement the transactional requirement mentioned in the class javadoc, we only commit
+ * UI changes to the actual view hierarchy once a batch of JS->Java calls have been completed.
+ * We know this is safe because all JS->Java calls that are triggered by a Java->JS call (e.g.
+ * the delivery of a touch event or execution of 'renderApplication') end up in a single
+ * JS->Java transaction.
+ *
+ * A better way to do this would be to have JS explicitly signal to this module when a UI
+ * transaction is done. Right now, though, this is how iOS does it, and we should probably
+ * update the JS and native code and make this change at the same time.
+ *
+ * TODO(5279396): Make JS UI library explicitly notify the native UI module of the end of a UI
+ * transaction using a standard native call
+ */
+ @Override
+ public void onBatchComplete() {
+ int batchId = mBatchId;
+ mBatchId++;
+
+ SystraceMessage.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "onBatchCompleteUI")
+ .arg("BatchId", batchId)
+ .flush();
+ try {
+ dispatchViewUpdates(batchId);
+ } finally {
+ Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE);
+ }
+ }
+
+ public void setUiManagerDebugListener(@Nullable NotThreadSafeUiManagerDebugListener listener) {
+ mUiManagerDebugListener = listener;
+ }
+
+ public EventDispatcher getEventDispatcher() {
+ return mEventDispatcher;
+ }
+
+ private void dispatchViewUpdates(final int batchId) {
+ for (int i = 0; i < mShadowNodeRegistry.getRootNodeCount(); i++) {
+ int tag = mShadowNodeRegistry.getRootTag(i);
+ ReactShadowNode cssRoot = mShadowNodeRegistry.getNode(tag);
+ notifyOnBeforeLayoutRecursive(cssRoot);
+ cssRoot.calculateLayout(mLayoutContext);
+ applyUpdatesRecursive(cssRoot, 0f, 0f);
+ }
+
+ mNativeViewHierarchyOptimizer.onBatchComplete();
+ mOperationsQueue.dispatchViewUpdates(batchId);
+ }
+
+ private void notifyOnBeforeLayoutRecursive(ReactShadowNode cssNode) {
+ if (!cssNode.hasUpdates()) {
+ return;
+ }
+ for (int i = 0; i < cssNode.getChildCount(); i++) {
+ notifyOnBeforeLayoutRecursive(cssNode.getChildAt(i));
+ }
+ cssNode.onBeforeLayout();
+ }
+
+ private void applyUpdatesRecursive(ReactShadowNode cssNode, float absoluteX, float absoluteY) {
+ if (!cssNode.hasUpdates()) {
+ return;
+ }
+
+ if (!cssNode.isVirtualAnchor()) {
+ for (int i = 0; i < cssNode.getChildCount(); i++) {
+ applyUpdatesRecursive(
+ cssNode.getChildAt(i),
+ absoluteX + cssNode.getLayoutX(),
+ absoluteY + cssNode.getLayoutY());
+ }
+ }
+
+ int tag = cssNode.getReactTag();
+ if (!mShadowNodeRegistry.isRootNode(tag)) {
+ cssNode.dispatchUpdates(
+ absoluteX,
+ absoluteY,
+ mOperationsQueue,
+ mNativeViewHierarchyOptimizer);
+
+ // notify JS about layout event if requested
+ if (cssNode.shouldNotifyOnLayout()) {
+ mEventDispatcher.dispatchEvent(
+ new OnLayoutEvent(
+ tag,
+ cssNode.getScreenX(),
+ cssNode.getScreenY(),
+ cssNode.getScreenWidth(),
+ cssNode.getScreenHeight()));
+ }
+ }
+ cssNode.markUpdateSeen();
+ }
+
+ /* package */ void notifyOnViewHierarchyUpdateEnqueued() {
+ if (mUiManagerDebugListener != null) {
+ mUiManagerDebugListener.onViewHierarchyUpdateEnqueued();
+ }
+ }
+
+ /* package */ void notifyOnViewHierarchyUpdateFinished() {
+ if (mUiManagerDebugListener != null) {
+ mUiManagerDebugListener.onViewHierarchyUpdateFinished();
+ }
+ }
+
+ @ReactMethod
+ public void sendAccessibilityEvent(int tag, int eventType) {
+ mOperationsQueue.enqueueSendAccessibilityEvent(tag, eventType);
+ }
+
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModuleConstants.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModuleConstants.java
new file mode 100644
index 00000000000000..3287ebf7854cd4
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModuleConstants.java
@@ -0,0 +1,157 @@
+/**
+ * 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.uimanager;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import android.text.InputType;
+import android.util.DisplayMetrics;
+import android.view.accessibility.AccessibilityEvent;
+import android.widget.ImageView;
+
+import com.facebook.react.common.MapBuilder;
+import com.facebook.react.uimanager.events.TouchEventType;
+
+/**
+ * Constants exposed to JS from {@link UIManagerModule}.
+ */
+/* package */ class UIManagerModuleConstants {
+
+ public static final String ACTION_DISMISSED = "dismissed";
+ public static final String ACTION_ITEM_SELECTED = "itemSelected";
+
+ /* package */ static Map getBubblingEventTypeConstants() {
+ return MapBuilder.builder()
+ .put(
+ "topChange",
+ MapBuilder.of(
+ "phasedRegistrationNames",
+ MapBuilder.of("bubbled", "onChange", "captured", "onChangeCapture")))
+ .put(
+ "topSelect",
+ MapBuilder.of(
+ "phasedRegistrationNames",
+ MapBuilder.of("bubbled", "onSelect", "captured", "onSelectCapture")))
+ .put(
+ TouchEventType.START.getJSEventName(),
+ MapBuilder.of(
+ "phasedRegistrationNames",
+ MapBuilder.of(
+ "bubbled",
+ "onTouchStart",
+ "captured",
+ "onTouchStartCapture")))
+ .put(
+ TouchEventType.MOVE.getJSEventName(),
+ MapBuilder.of(
+ "phasedRegistrationNames",
+ MapBuilder.of(
+ "bubbled",
+ "onTouchMove",
+ "captured",
+ "onTouchMoveCapture")))
+ .put(
+ TouchEventType.END.getJSEventName(),
+ MapBuilder.of(
+ "phasedRegistrationNames",
+ MapBuilder.of(
+ "bubbled",
+ "onTouchEnd",
+ "captured",
+ "onTouchEndCapture")))
+ .build();
+ }
+
+ /* package */ static Map getDirectEventTypeConstants() {
+ return MapBuilder.builder()
+ .put("topSelectionChange", MapBuilder.of("registrationName", "onSelectionChange"))
+ .put("topLoadingStart", MapBuilder.of("registrationName", "onLoadingStart"))
+ .put("topLoadingFinish", MapBuilder.of("registrationName", "onLoadingFinish"))
+ .put("topLoadingError", MapBuilder.of("registrationName", "onLoadingError"))
+ .put("topLayout", MapBuilder.of("registrationName", "onLayout"))
+ .build();
+ }
+
+ public static Map getConstants(DisplayMetrics displayMetrics) {
+ HashMap constants = new HashMap();
+ constants.put(
+ "UIView",
+ MapBuilder.of(
+ "ContentMode",
+ MapBuilder.of(
+ "ScaleAspectFit",
+ ImageView.ScaleType.CENTER_INSIDE.ordinal(),
+ "ScaleAspectFill",
+ ImageView.ScaleType.CENTER_CROP.ordinal())));
+
+ constants.put(
+ "UIText",
+ MapBuilder.of(
+ "AutocapitalizationType",
+ MapBuilder.of(
+ "none",
+ InputType.TYPE_CLASS_TEXT,
+ "characters",
+ InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS,
+ "words",
+ InputType.TYPE_TEXT_FLAG_CAP_WORDS,
+ "sentences",
+ InputType.TYPE_TEXT_FLAG_CAP_SENTENCES)));
+
+ constants.put(
+ "Dimensions",
+ MapBuilder.of(
+ "windowPhysicalPixels",
+ MapBuilder.of(
+ "width",
+ displayMetrics.widthPixels,
+ "height",
+ displayMetrics.heightPixels,
+ "scale",
+ displayMetrics.density,
+ "fontScale",
+ displayMetrics.scaledDensity,
+ "densityDpi",
+ displayMetrics.densityDpi)));
+
+ constants.put(
+ "StyleConstants",
+ MapBuilder.of(
+ "PointerEventsValues",
+ MapBuilder.of(
+ "none",
+ PointerEvents.NONE.ordinal(),
+ "boxNone",
+ PointerEvents.BOX_NONE.ordinal(),
+ "boxOnly",
+ PointerEvents.BOX_ONLY.ordinal(),
+ "unspecified",
+ PointerEvents.AUTO.ordinal())));
+
+ constants.put(
+ "PopupMenu",
+ MapBuilder.of(
+ ACTION_DISMISSED,
+ ACTION_DISMISSED,
+ ACTION_ITEM_SELECTED,
+ ACTION_ITEM_SELECTED));
+
+ constants.put(
+ "AccessibilityEventTypes",
+ MapBuilder.of(
+ "typeWindowStateChanged",
+ AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED,
+ "typeViewClicked",
+ AccessibilityEvent.TYPE_VIEW_CLICKED));
+
+ return constants;
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModuleConstantsHelper.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModuleConstantsHelper.java
new file mode 100644
index 00000000000000..61ca838c295412
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModuleConstantsHelper.java
@@ -0,0 +1,100 @@
+/**
+ * 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.uimanager;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import android.util.DisplayMetrics;
+
+import com.facebook.react.common.MapBuilder;
+
+/**
+ * Helps generate constants map for {@link UIManagerModule} by collecting and merging constants from
+ * registered view managers.
+ */
+/* package */ class UIManagerModuleConstantsHelper {
+
+ private static final String CUSTOM_BUBBLING_EVENT_TYPES_KEY = "customBubblingEventTypes";
+ private static final String CUSTOM_DIRECT_EVENT_TYPES_KEY = "customDirectEventTypes";
+
+ /**
+ * Generates map of constants that is then exposed by {@link UIManagerModule}. The constants map
+ * contains the following predefined fields for 'customBubblingEventTypes' and
+ * 'customDirectEventTypes'. Provided list of {@param viewManagers} is then used to populate
+ * content of those predefined fields using
+ * {@link ViewManager#getExportedCustomBubblingEventTypeConstants} and
+ * {@link ViewManager#getExportedCustomDirectEventTypeConstants} respectively. Each view manager
+ * is in addition allowed to expose viewmanager-specific constants that are placed under the key
+ * that corresponds to the view manager's name (see {@link ViewManager#getName}). Constants are
+ * merged into the map of {@link UIManagerModule} base constants that is stored in
+ * {@link UIManagerModuleConstants}.
+ * TODO(6845124): Create a test for this
+ */
+ /* package */ static Map createConstants(
+ DisplayMetrics displayMetrics,
+ List viewManagers) {
+ Map constants = UIManagerModuleConstants.getConstants(displayMetrics);
+ Map bubblingEventTypesConstants = UIManagerModuleConstants.getBubblingEventTypeConstants();
+ Map directEventTypesConstants = UIManagerModuleConstants.getDirectEventTypeConstants();
+
+ for (ViewManager viewManager : viewManagers) {
+ Map viewManagerBubblingEvents = viewManager.getExportedCustomBubblingEventTypeConstants();
+ if (viewManagerBubblingEvents != null) {
+ recursiveMerge(bubblingEventTypesConstants, viewManagerBubblingEvents);
+ }
+ Map viewManagerDirectEvents = viewManager.getExportedCustomDirectEventTypeConstants();
+ if (viewManagerDirectEvents != null) {
+ recursiveMerge(directEventTypesConstants, viewManagerDirectEvents);
+ }
+ Map viewManagerConstants = MapBuilder.newHashMap();
+ Map customViewConstants = viewManager.getExportedViewConstants();
+ if (customViewConstants != null) {
+ viewManagerConstants.put("Constants", customViewConstants);
+ }
+ Map viewManagerCommands = viewManager.getCommandsMap();
+ if (viewManagerCommands != null) {
+ viewManagerConstants.put("Commands", viewManagerCommands);
+ }
+ Map viewManagerNativeProps = viewManager.getNativeProps();
+ if (!viewManagerNativeProps.isEmpty()) {
+ Map nativeProps = new HashMap<>();
+ for (Map.Entry entry : viewManagerNativeProps.entrySet()) {
+ nativeProps.put(entry.getKey(), entry.getValue().toString());
+ }
+ viewManagerConstants.put("NativeProps", nativeProps);
+ }
+ if (!viewManagerConstants.isEmpty()) {
+ constants.put(viewManager.getName(), viewManagerConstants);
+ }
+ }
+
+ constants.put(CUSTOM_BUBBLING_EVENT_TYPES_KEY, bubblingEventTypesConstants);
+ constants.put(CUSTOM_DIRECT_EVENT_TYPES_KEY, directEventTypesConstants);
+
+ return constants;
+ }
+
+ /**
+ * Merges {@param source} map into {@param dest} map recursively
+ */
+ private static void recursiveMerge(Map dest, Map source) {
+ for (Object key : source.keySet()) {
+ Object sourceValue = source.get(key);
+ Object destValue = dest.get(key);
+ if (destValue != null && (sourceValue instanceof Map) && (destValue instanceof Map)) {
+ recursiveMerge((Map) destValue, (Map) sourceValue);
+ } else {
+ dest.put(key, sourceValue);
+ }
+ }
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIProp.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIProp.java
new file mode 100644
index 00000000000000..ef10ca1eaa5f0e
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIProp.java
@@ -0,0 +1,52 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+package com.facebook.react.uimanager;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+/**
+ * Annotation which is used to mark native UI properties that are exposed to
+ * JS. {@link ViewManager#getNativeProps} traverses the fields of its
+ * subclasses and extracts the {@code UIProp} annotation data to generate the
+ * {@code NativeProps} map. Example:
+ *
+ * {@code
+ * @UIProp(UIProp.Type.BOOLEAN) public static final String PROP_FOO = "foo";
+ * @UIProp(UIProp.Type.STRING) public static final String PROP_BAR = "bar";
+ * }
+ */
+@Target(ElementType.FIELD)
+@Retention(RUNTIME)
+public @interface UIProp {
+ Type value();
+
+ public static enum Type {
+ BOOLEAN("boolean"),
+ NUMBER("number"),
+ STRING("String"),
+ MAP("Map"),
+ ARRAY("Array");
+
+ private final String mType;
+
+ Type(String type) {
+ mType = type;
+ }
+
+ @Override
+ public String toString() {
+ return mType;
+ }
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIViewOperationQueue.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIViewOperationQueue.java
new file mode 100644
index 00000000000000..60891ae2d18237
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIViewOperationQueue.java
@@ -0,0 +1,631 @@
+/**
+ * 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.uimanager;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
+
+import java.util.ArrayList;
+
+import com.facebook.react.animation.Animation;
+import com.facebook.react.animation.AnimationRegistry;
+import com.facebook.react.bridge.Callback;
+import com.facebook.react.bridge.ReactContext;
+import com.facebook.react.bridge.ReactApplicationContext;
+import com.facebook.react.bridge.ReadableArray;
+import com.facebook.systrace.Systrace;
+import com.facebook.systrace.SystraceMessage;
+
+/**
+ * This class acts as a buffer for command executed on {@link NativeViewHierarchyManager} or on
+ * {@link AnimationRegistry}. It expose similar methods as mentioned classes but instead of
+ * executing commands immediately it enqueues those operations in a queue that is then flushed from
+ * {@link UIManagerModule} once JS batch of ui operations is finished. This is to make sure that we
+ * execute all the JS operation coming from a single batch a single loop of the main (UI) android
+ * looper.
+ *
+ * TODO(7135923): Pooling of operation objects
+ * TODO(5694019): Consider a better data structure for operations queue to save on allocations
+ */
+public class UIViewOperationQueue {
+
+ private final int[] mMeasureBuffer = new int[4];
+
+ /**
+ * A mutation or animation operation on the view hierarchy.
+ */
+ private interface UIOperation {
+
+ void execute();
+ }
+
+ /**
+ * A spec for an operation on the native View hierarchy.
+ */
+ private abstract class ViewOperation implements UIOperation {
+
+ public int mTag;
+
+ public ViewOperation(int tag) {
+ mTag = tag;
+ }
+ }
+
+ private final class RemoveRootViewOperation extends ViewOperation {
+
+ public RemoveRootViewOperation(int tag) {
+ super(tag);
+ }
+
+ @Override
+ public void execute() {
+ mNativeViewHierarchyManager.removeRootView(mTag);
+ }
+ }
+
+ private final class UpdatePropertiesOperation extends ViewOperation {
+
+ private final CatalystStylesDiffMap mProps;
+
+ private UpdatePropertiesOperation(int tag, CatalystStylesDiffMap props) {
+ super(tag);
+ mProps = props;
+ }
+
+ @Override
+ public void execute() {
+ mNativeViewHierarchyManager.updateProperties(mTag, mProps);
+ }
+ }
+
+ /**
+ * Operation for updating native view's position and size. The operation is not created directly
+ * by a {@link UIManagerModule} call from JS. Instead it gets inflated using computed position
+ * and size values by CSSNode hierarchy.
+ */
+ private final class UpdateLayoutOperation extends ViewOperation {
+
+ private final int mParentTag, mX, mY, mWidth, mHeight;
+
+ public UpdateLayoutOperation(
+ int parentTag,
+ int tag,
+ int x,
+ int y,
+ int width,
+ int height) {
+ super(tag);
+ mParentTag = parentTag;
+ mX = x;
+ mY = y;
+ mWidth = width;
+ mHeight = height;
+ }
+
+ @Override
+ public void execute() {
+ mNativeViewHierarchyManager.updateLayout(mParentTag, mTag, mX, mY, mWidth, mHeight);
+ }
+ }
+
+ private final class CreateViewOperation extends ViewOperation {
+
+ private final int mRootViewTagForContext;
+ private final String mClassName;
+ private final @Nullable CatalystStylesDiffMap mInitialProps;
+
+ public CreateViewOperation(
+ int rootViewTagForContext,
+ int tag,
+ String className,
+ @Nullable CatalystStylesDiffMap initialProps) {
+ super(tag);
+ mRootViewTagForContext = rootViewTagForContext;
+ mClassName = className;
+ mInitialProps = initialProps;
+ }
+
+ @Override
+ public void execute() {
+ mNativeViewHierarchyManager.createView(
+ mRootViewTagForContext,
+ mTag,
+ mClassName,
+ mInitialProps);
+ }
+ }
+
+ private final class ManageChildrenOperation extends ViewOperation {
+
+ private final @Nullable int[] mIndicesToRemove;
+ private final @Nullable ViewAtIndex[] mViewsToAdd;
+ private final @Nullable int[] mTagsToDelete;
+
+ public ManageChildrenOperation(
+ int tag,
+ @Nullable int[] indicesToRemove,
+ @Nullable ViewAtIndex[] viewsToAdd,
+ @Nullable int[] tagsToDelete) {
+ super(tag);
+ mIndicesToRemove = indicesToRemove;
+ mViewsToAdd = viewsToAdd;
+ mTagsToDelete = tagsToDelete;
+ }
+
+ @Override
+ public void execute() {
+ mNativeViewHierarchyManager.manageChildren(
+ mTag,
+ mIndicesToRemove,
+ mViewsToAdd,
+ mTagsToDelete);
+ }
+ }
+
+ private final class UpdateViewExtraData extends ViewOperation {
+
+ private final Object mExtraData;
+
+ public UpdateViewExtraData(int tag, Object extraData) {
+ super(tag);
+ mExtraData = extraData;
+ }
+
+ @Override
+ public void execute() {
+ mNativeViewHierarchyManager.updateViewExtraData(mTag, mExtraData);
+ }
+ }
+
+ private final class ChangeJSResponderOperation extends ViewOperation {
+
+ private final boolean mBlockNativeResponder;
+ private final boolean mClearResponder;
+
+ public ChangeJSResponderOperation(
+ int tag,
+ boolean clearResponder,
+ boolean blockNativeResponder) {
+ super(tag);
+ mClearResponder = clearResponder;
+ mBlockNativeResponder = blockNativeResponder;
+ }
+
+ @Override
+ public void execute() {
+ if (!mClearResponder) {
+ mNativeViewHierarchyManager.setJSResponder(mTag, mBlockNativeResponder);
+ } else {
+ mNativeViewHierarchyManager.clearJSResponder();
+ }
+ }
+ }
+
+ private final class DispatchCommandOperation extends ViewOperation {
+
+ private final int mCommand;
+ private final @Nullable ReadableArray mArgs;
+
+ public DispatchCommandOperation(int tag, int command, @Nullable ReadableArray args) {
+ super(tag);
+ mCommand = command;
+ mArgs = args;
+ }
+
+ @Override
+ public void execute() {
+ mNativeViewHierarchyManager.dispatchCommand(mTag, mCommand, mArgs);
+ }
+ }
+
+ private final class ShowPopupMenuOperation extends ViewOperation {
+
+ private final ReadableArray mItems;
+ private final Callback mSuccess;
+
+ public ShowPopupMenuOperation(
+ int tag,
+ ReadableArray items,
+ Callback success) {
+ super(tag);
+ mItems = items;
+ mSuccess = success;
+ }
+
+ @Override
+ public void execute() {
+ mNativeViewHierarchyManager.showPopupMenu(mTag, mItems, mSuccess);
+ }
+ }
+
+ /**
+ * A spec for animation operations (add/remove)
+ */
+ private static abstract class AnimationOperation implements UIViewOperationQueue.UIOperation {
+
+ protected final int mAnimationID;
+
+ public AnimationOperation(int animationID) {
+ mAnimationID = animationID;
+ }
+ }
+
+ private class RegisterAnimationOperation extends AnimationOperation {
+
+ private final Animation mAnimation;
+
+ private RegisterAnimationOperation(Animation animation) {
+ super(animation.getAnimationID());
+ mAnimation = animation;
+ }
+
+ @Override
+ public void execute() {
+ mAnimationRegistry.registerAnimation(mAnimation);
+ }
+ }
+
+ private class AddAnimationOperation extends AnimationOperation {
+ private final int mReactTag;
+ private final Callback mSuccessCallback;
+
+ private AddAnimationOperation(int reactTag, int animationID, Callback successCallback) {
+ super(animationID);
+ mReactTag = reactTag;
+ mSuccessCallback = successCallback;
+ }
+
+ @Override
+ public void execute() {
+ Animation animation = mAnimationRegistry.getAnimation(mAnimationID);
+ if (animation != null) {
+ mNativeViewHierarchyManager.startAnimationForNativeView(
+ mReactTag,
+ animation,
+ mSuccessCallback);
+ } else {
+ // node or animation not found
+ // TODO(5712813): cleanup callback in JS callbacks table in case of an error
+ throw new IllegalViewOperationException("Animation with id " + mAnimationID
+ + " was not found");
+ }
+ }
+ }
+
+ private final class RemoveAnimationOperation extends AnimationOperation {
+
+ private RemoveAnimationOperation(int animationID) {
+ super(animationID);
+ }
+
+ @Override
+ public void execute() {
+ Animation animation = mAnimationRegistry.getAnimation(mAnimationID);
+ if (animation != null) {
+ animation.cancel();
+ }
+ }
+ }
+
+ private final class MeasureOperation implements UIOperation {
+
+ private final int mReactTag;
+ private final Callback mCallback;
+
+ private MeasureOperation(
+ final int reactTag,
+ final Callback callback) {
+ super();
+ mReactTag = reactTag;
+ mCallback = callback;
+ }
+
+ @Override
+ public void execute() {
+ try {
+ mNativeViewHierarchyManager.measure(mReactTag, mMeasureBuffer);
+ } catch (NoSuchNativeViewException e) {
+ // Invoke with no args to signal failure and to allow JS to clean up the callback
+ // handle.
+ mCallback.invoke();
+ return;
+ }
+
+ float x = PixelUtil.toDIPFromPixel(mMeasureBuffer[0]);
+ float y = PixelUtil.toDIPFromPixel(mMeasureBuffer[1]);
+ float width = PixelUtil.toDIPFromPixel(mMeasureBuffer[2]);
+ float height = PixelUtil.toDIPFromPixel(mMeasureBuffer[3]);
+ mCallback.invoke(0, 0, width, height, x, y);
+ }
+ }
+
+ private ArrayList mOperations = new ArrayList<>();
+
+ private final class FindTargetForTouchOperation implements UIOperation {
+
+ private final int mReactTag;
+ private final float mTargetX;
+ private final float mTargetY;
+ private final Callback mCallback;
+
+ private FindTargetForTouchOperation(
+ final int reactTag,
+ final float targetX,
+ final float targetY,
+ final Callback callback) {
+ super();
+ mReactTag = reactTag;
+ mTargetX = targetX;
+ mTargetY = targetY;
+ mCallback = callback;
+ }
+
+ @Override
+ public void execute() {
+ try {
+ mNativeViewHierarchyManager.measure(
+ mReactTag,
+ mMeasureBuffer);
+ } catch (IllegalViewOperationException e) {
+ mCallback.invoke();
+ return;
+ }
+
+ // Because React coordinates are relative to root container, and measure() operates
+ // on screen coordinates, we need to offset values using root container location.
+ final float containerX = (float) mMeasureBuffer[0];
+ final float containerY = (float) mMeasureBuffer[1];
+
+ final int touchTargetReactTag = mNativeViewHierarchyManager.findTargetTagForTouch(
+ mReactTag,
+ PixelUtil.toPixelFromDIP(mTargetX) + containerX,
+ PixelUtil.toPixelFromDIP(mTargetY) + containerY);
+
+ try {
+ mNativeViewHierarchyManager.measure(
+ touchTargetReactTag,
+ mMeasureBuffer);
+ } catch (IllegalViewOperationException e) {
+ mCallback.invoke();
+ return;
+ }
+
+ float x = PixelUtil.toDIPFromPixel(mMeasureBuffer[0] - containerX);
+ float y = PixelUtil.toDIPFromPixel(mMeasureBuffer[1] - containerY);
+ float width = PixelUtil.toDIPFromPixel(mMeasureBuffer[2]);
+ float height = PixelUtil.toDIPFromPixel(mMeasureBuffer[3]);
+ mCallback.invoke(touchTargetReactTag, x, y, width, height);
+ }
+ }
+
+ private final class SendAccessibilityEvent extends ViewOperation {
+
+ private final int mEventType;
+
+ private SendAccessibilityEvent(int tag, int eventType) {
+ super(tag);
+ mEventType = eventType;
+ }
+
+ @Override
+ public void execute() {
+ mNativeViewHierarchyManager.sendAccessibilityEvent(mTag, mEventType);
+ }
+ }
+
+ private final UIManagerModule mUIManagerModule;
+ private final NativeViewHierarchyManager mNativeViewHierarchyManager;
+ private final AnimationRegistry mAnimationRegistry;
+
+ private final Object mDispatchRunnablesLock = new Object();
+ private final DispatchUIFrameCallback mDispatchUIFrameCallback;
+
+ @GuardedBy("mDispatchRunnablesLock")
+ private final ArrayList mDispatchUIRunnables = new ArrayList<>();
+
+ /* package */ UIViewOperationQueue(
+ ReactApplicationContext reactContext,
+ UIManagerModule uiManagerModule,
+ NativeViewHierarchyManager nativeViewHierarchyManager,
+ AnimationRegistry animationRegistry) {
+ mUIManagerModule = uiManagerModule;
+ mNativeViewHierarchyManager = nativeViewHierarchyManager;
+ mAnimationRegistry = animationRegistry;
+ mDispatchUIFrameCallback = new DispatchUIFrameCallback(reactContext);
+ }
+
+ public boolean isEmpty() {
+ return mOperations.isEmpty();
+ }
+
+ public void enqueueRemoveRootView(int rootViewTag) {
+ mOperations.add(new RemoveRootViewOperation(rootViewTag));
+ }
+
+ public void enqueueSetJSResponder(int reactTag, boolean blockNativeResponder) {
+ mOperations.add(
+ new ChangeJSResponderOperation(reactTag, false /*clearResponder*/, blockNativeResponder));
+ }
+
+ public void enqueueClearJSResponder() {
+ // Tag is 0 because JSResponderHandler doesn't need one in order to clear the responder.
+ mOperations.add(new ChangeJSResponderOperation(0, true /*clearResponder*/, false));
+ }
+
+ public void enqueueDispatchCommand(
+ int reactTag,
+ int commandId,
+ ReadableArray commandArgs) {
+ mOperations.add(new DispatchCommandOperation(reactTag, commandId, commandArgs));
+ }
+
+ public void enqueueUpdateExtraData(int reactTag, Object extraData) {
+ mOperations.add(new UpdateViewExtraData(reactTag, extraData));
+ }
+
+ public void enqueueShowPopupMenu(
+ int reactTag,
+ ReadableArray items,
+ Callback error,
+ Callback success) {
+ mOperations.add(new ShowPopupMenuOperation(reactTag, items, success));
+ }
+
+ public void enqueueCreateView(
+ int rootViewTagForContext,
+ int viewReactTag,
+ String viewClassName,
+ @Nullable CatalystStylesDiffMap initialProps) {
+ mOperations.add(
+ new CreateViewOperation(
+ rootViewTagForContext,
+ viewReactTag,
+ viewClassName,
+ initialProps));
+ }
+
+ public void enqueueUpdateProperties(int reactTag, String className, CatalystStylesDiffMap props) {
+ mOperations.add(new UpdatePropertiesOperation(reactTag, props));
+ }
+
+ public void enqueueUpdateLayout(
+ int parentTag,
+ int reactTag,
+ int x,
+ int y,
+ int width,
+ int height) {
+ mOperations.add(
+ new UpdateLayoutOperation(parentTag, reactTag, x, y, width, height));
+ }
+
+ public void enqueueManageChildren(
+ int reactTag,
+ @Nullable int[] indicesToRemove,
+ @Nullable ViewAtIndex[] viewsToAdd,
+ @Nullable int[] tagsToDelete) {
+ mOperations.add(
+ new ManageChildrenOperation(reactTag, indicesToRemove, viewsToAdd, tagsToDelete));
+ }
+
+ public void enqueueRegisterAnimation(Animation animation) {
+ mOperations.add(new RegisterAnimationOperation(animation));
+ }
+
+ public void enqueueAddAnimation(
+ final int reactTag,
+ final int animationID,
+ final Callback onSuccess) {
+ mOperations.add(new AddAnimationOperation(reactTag, animationID, onSuccess));
+ }
+
+ public void enqueueRemoveAnimation(int animationID) {
+ mOperations.add(new RemoveAnimationOperation(animationID));
+ }
+
+ public void enqueueMeasure(
+ final int reactTag,
+ final Callback callback) {
+ mOperations.add(
+ new MeasureOperation(reactTag, callback));
+ }
+
+ public void enqueueFindTargetForTouch(
+ final int reactTag,
+ final float targetX,
+ final float targetY,
+ final Callback callback) {
+ mOperations.add(
+ new FindTargetForTouchOperation(reactTag, targetX, targetY, callback));
+ }
+
+ public void enqueueSendAccessibilityEvent(int tag, int eventType) {
+ mOperations.add(new SendAccessibilityEvent(tag, eventType));
+ }
+
+ /* package */ void dispatchViewUpdates(final int batchId) {
+ // Store the current operation queues to dispatch and create new empty ones to continue
+ // receiving new operations
+ final ArrayList operations = mOperations.isEmpty() ? null : mOperations;
+ if (operations != null) {
+ mOperations = new ArrayList<>();
+ }
+
+ mUIManagerModule.notifyOnViewHierarchyUpdateEnqueued();
+
+ synchronized (mDispatchRunnablesLock) {
+ mDispatchUIRunnables.add(
+ new Runnable() {
+ @Override
+ public void run() {
+ SystraceMessage.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "DispatchUI")
+ .arg("BatchId", batchId)
+ .flush();
+ try {
+ if (operations != null) {
+ for (int i = 0; i < operations.size(); i++) {
+ operations.get(i).execute();
+ }
+ }
+ mUIManagerModule.notifyOnViewHierarchyUpdateFinished();
+ } finally {
+ Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE);
+ }
+ }
+ });
+ }
+ }
+
+ /* package */ void resumeFrameCallback() {
+ ReactChoreographer.getInstance()
+ .postFrameCallback(ReactChoreographer.CallbackType.DISPATCH_UI, mDispatchUIFrameCallback);
+ }
+
+ /* package */ void pauseFrameCallback() {
+
+ ReactChoreographer.getInstance()
+ .removeFrameCallback(ReactChoreographer.CallbackType.DISPATCH_UI, mDispatchUIFrameCallback);
+ }
+
+ /**
+ * Choreographer FrameCallback responsible for actually dispatching view updates on the UI thread
+ * that were enqueued via {@link #dispatchViewUpdates(int)}. The reason we don't just enqueue
+ * directly to the UI thread from that method is to make sure our Runnables actually run before
+ * the next traversals happen:
+ *
+ * ViewRootImpl#scheduleTraversals (which is called from invalidate, requestLayout, etc) calls
+ * Looper#postSyncBarrier which keeps any UI thread looper messages from being processed until
+ * that barrier is removed during the next traversal. That means, depending on when we get updates
+ * from JS and what else is happening on the UI thread, we can sometimes try to post this runnable
+ * after ViewRootImpl has posted a barrier.
+ *
+ * Using a Choreographer callback (which runs immediately before traversals), we guarantee we run
+ * before the next traversal.
+ */
+ private class DispatchUIFrameCallback extends GuardedChoreographerFrameCallback {
+
+ private DispatchUIFrameCallback(ReactContext reactContext) {
+ super(reactContext);
+ }
+
+ @Override
+ public void doFrameGuarded(long frameTimeNanos) {
+ synchronized (mDispatchRunnablesLock) {
+ for (int i = 0; i < mDispatchUIRunnables.size(); i++) {
+ mDispatchUIRunnables.get(i).run();
+ }
+ mDispatchUIRunnables.clear();
+ }
+
+ ReactChoreographer.getInstance().postFrameCallback(
+ ReactChoreographer.CallbackType.DISPATCH_UI, this);
+ }
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewAtIndex.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewAtIndex.java
new file mode 100644
index 00000000000000..6ceef2632d36f4
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewAtIndex.java
@@ -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.uimanager;
+
+import java.util.Comparator;
+
+/**
+ * Data structure that couples view tag to it's index in parent view. Used for managing children
+ * operation.
+ */
+/* package */ class ViewAtIndex {
+ public static Comparator COMPARATOR = new Comparator() {
+ @Override
+ public int compare(ViewAtIndex lhs, ViewAtIndex rhs) {
+ return lhs.mIndex - rhs.mIndex;
+ }
+ };
+
+ public final int mTag;
+ public final int mIndex;
+
+ public ViewAtIndex(int tag, int index) {
+ mTag = tag;
+ mIndex = index;
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewDefaults.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewDefaults.java
new file mode 100644
index 00000000000000..5ee3cc36ab2f00
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewDefaults.java
@@ -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.uimanager;
+
+/**
+ * Default property values for Views to be shared between Views and ShadowViews.
+ */
+public class ViewDefaults {
+
+ public static final float FONT_SIZE_SP = 14.0f;
+ public static final int LINE_HEIGHT = 0;
+ public static final int NUMBER_OF_LINES = Integer.MAX_VALUE;
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewGroupManager.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewGroupManager.java
new file mode 100644
index 00000000000000..eb0b3ee63e22e5
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewGroupManager.java
@@ -0,0 +1,65 @@
+/**
+ * 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.uimanager;
+
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * Class providing children management API for view managers of classes extending ViewGroup.
+ */
+public abstract class ViewGroupManager
+ extends ViewManager {
+
+ @Override
+ public ReactShadowNode createCSSNodeInstance() {
+ return new ReactShadowNode();
+ }
+
+ @Override
+ public void updateView(T root, CatalystStylesDiffMap props) {
+ BaseViewPropertyApplicator.applyCommonViewProperties(root, props);
+ }
+
+ @Override
+ public void updateExtraData(T root, Object extraData) {
+ }
+
+ public void addView(T parent, View child, int index) {
+ parent.addView(child, index);
+ }
+
+ public int getChildCount(T parent) {
+ return parent.getChildCount();
+ }
+
+ public View getChildAt(T parent, int index) {
+ return parent.getChildAt(index);
+ }
+
+ public void removeView(T parent, View child) {
+ parent.removeView(child);
+ }
+
+ /**
+ * Returns whether this View type needs to handle laying out its own children instead of
+ * deferring to the standard css-layout algorithm.
+ * Returns true for the layout to *not* be automatically invoked. Instead onLayout will be
+ * invoked as normal and it is the View instance's responsibility to properly call layout on its
+ * children.
+ * Returns false for the default behavior of automatically laying out children without going
+ * through the ViewGroup's onLayout method. In that case, onLayout for this View type must *not*
+ * call layout on its children.
+ */
+ public boolean needsCustomLayoutForChildren() {
+ return false;
+ }
+
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManager.java
new file mode 100644
index 00000000000000..eaf442d232ca73
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManager.java
@@ -0,0 +1,216 @@
+/**
+ * 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.uimanager;
+
+import javax.annotation.Nullable;
+
+import java.lang.reflect.Field;
+import java.util.HashMap;
+import java.util.Map;
+
+import android.view.View;
+
+import com.facebook.csslayout.CSSNode;
+import com.facebook.react.touch.CatalystInterceptingViewGroup;
+import com.facebook.react.touch.JSResponderHandler;
+import com.facebook.react.bridge.ReactContext;
+import com.facebook.react.bridge.ReadableArray;
+
+/**
+ * Class responsible for knowing how to create and update catalyst Views of a given type. It is also
+ * responsible for creating and updating CSSNode subclasses used for calculating position and size
+ * for the corresponding native view.
+ */
+public abstract class ViewManager {
+
+ private static final Map> CLASS_PROP_CACHE = new HashMap<>();
+
+ /**
+ * Creates a view and installs event emitters on it.
+ */
+ public final T createView(
+ ThemedReactContext reactContext,
+ JSResponderHandler jsResponderHandler) {
+ T view = createViewInstance(reactContext);
+ addEventEmitters(reactContext, view);
+ if (view instanceof CatalystInterceptingViewGroup) {
+ ((CatalystInterceptingViewGroup) view).setOnInterceptTouchEventListener(jsResponderHandler);
+ }
+ return view;
+ }
+
+ /**
+ * @return the name of this view manager. This will be the name used to reference this view
+ * manager from JavaScript in createReactNativeComponentClass.
+ */
+ public abstract String getName();
+
+ /**
+ * This method should return a subclass of {@link CSSNode} which will be then used for measuring
+ * position and size of the view. In mose of the cases this should just return an instance of
+ * {@link CSSNode}
+ */
+ public abstract C createCSSNodeInstance();
+
+ /**
+ * Subclasses should return a new View instance of the proper type.
+ * @param reactContext
+ */
+ protected abstract T createViewInstance(ThemedReactContext reactContext);
+
+ /**
+ * Called when view is detached from view hierarchy and allows for some additional cleanup by
+ * the {@link ViewManager} subclass.
+ */
+ public void onDropViewInstance(ThemedReactContext reactContext, T view) {
+ }
+
+ /**
+ * Subclasses can override this method to install custom event emitters on the given View. You
+ * might want to override this method if your view needs to emit events besides basic touch events
+ * to JS (e.g. scroll events).
+ */
+ protected void addEventEmitters(ThemedReactContext reactContext, T view) {
+ }
+
+ /**
+ * Subclass should use this method to populate native view with updated style properties. In case
+ * when a certain property is present in {@param props} map but the value is null, this property
+ * should be reset to the default value
+ */
+ public abstract void updateView(T root, CatalystStylesDiffMap props);
+
+ /**
+ * Subclasses can implement this method to receive an optional extra data enqueued from the
+ * corresponding instance of {@link ReactShadowNode} in
+ * {@link ReactShadowNode#onCollectExtraUpdates}.
+ *
+ * Since css layout step and ui updates can be executed in separate thread apart of setting
+ * x/y/width/height this is the recommended and thread-safe way of passing extra data from css
+ * node to the native view counterpart.
+ *
+ * TODO(7247021): Replace updateExtraData with generic update props mechanism after D2086999
+ */
+ public abstract void updateExtraData(T root, Object extraData);
+
+ /**
+ * Subclasses may use this method to receive events/commands directly from JS through the
+ * {@link UIManager}. Good example of such a command would be {@code scrollTo} request with
+ * coordinates for a {@link ScrollView} or {@code goBack} request for a {@link WebView} instance.
+ *
+ * @param root View instance that should receive the command
+ * @param commandId code of the command
+ * @param args optional arguments for the command
+ */
+ public void receiveCommand(T root, int commandId, @Nullable ReadableArray args) {
+ }
+
+ /**
+ * Subclasses of {@link ViewManager} that expect to receive commands through
+ * {@link UIManagerModule#dispatchViewManagerCommand} should override this method returning the
+ * map between names of the commands and IDs that are then used in {@link #receiveCommand} method
+ * whenever the command is dispatched for this particular {@link ViewManager}.
+ *
+ * As an example we may consider {@link ReactWebViewManager} that expose the following commands:
+ * goBack, goForward, reload. In this case the map returned from {@link #getCommandsMap} from
+ * {@link ReactWebViewManager} will look as follows:
+ * {
+ * "goBack": 1,
+ * "goForward": 2,
+ * "reload": 3,
+ * }
+ *
+ * Now assuming that "reload" command is dispatched through {@link UIManagerModule} we trigger
+ * {@link ReactWebViewManager#receiveCommand} passing "3" as {@code commandId} argument.
+ *
+ * @return map of string to int mapping of the expected commands
+ */
+ public @Nullable Map getCommandsMap() {
+ return null;
+ }
+
+ /**
+ * Returns a map of config data passed to JS that defines eligible events that can be placed on
+ * native views. This should return bubbling directly-dispatched event types and specify what
+ * names should be used to subscribe to either form (bubbling/capturing).
+ *
+ * Returned map should be of the form:
+ * {
+ * "onTwirl": {
+ * "phasedRegistrationNames": {
+ * "bubbled": "onTwirl",
+ * "captured": "onTwirlCaptured"
+ * }
+ * }
+ * }
+ */
+ public @Nullable Map getExportedCustomBubblingEventTypeConstants() {
+ return null;
+ }
+
+ /**
+ * Returns a map of config data passed to JS that defines eligible events that can be placed on
+ * native views. This should return non-bubbling directly-dispatched event types.
+ *
+ * Returned map should be of the form:
+ * {
+ * "onTwirl": {
+ * "registrationName": "onTwirl"
+ * }
+ * }
+ */
+ public @Nullable Map getExportedCustomDirectEventTypeConstants() {
+ return null;
+ }
+
+ /**
+ * Returns a map of view-specific constants that are injected to JavaScript. These constants are
+ * made accessible via UIManager..Constants.
+ */
+ public @Nullable Map getExportedViewConstants() {
+ return null;
+ }
+
+ public Map getNativeProps() {
+ Map nativeProps = new HashMap<>();
+ Class cls = getClass();
+ while (cls.getSuperclass() != null) {
+ Map props = getNativePropsForClass(cls);
+ for (Map.Entry entry : props.entrySet()) {
+ nativeProps.put(entry.getKey(), entry.getValue());
+ }
+ cls = cls.getSuperclass();
+ }
+ return nativeProps;
+ }
+
+ private Map getNativePropsForClass(Class cls) {
+ Map props = CLASS_PROP_CACHE.get(cls);
+ if (props != null) {
+ return props;
+ }
+ props = new HashMap<>();
+ for (Field f : cls.getDeclaredFields()) {
+ UIProp annotation = f.getAnnotation(UIProp.class);
+ if (annotation != null) {
+ UIProp.Type type = annotation.value();
+ try {
+ String name = (String) f.get(this);
+ props.put(name, type);
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException(
+ "UIProp " + cls.getName() + "." + f.getName() + " must be public.");
+ }
+ }
+ }
+ CLASS_PROP_CACHE.put(cls, props);
+ return props;
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManagerRegistry.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManagerRegistry.java
new file mode 100644
index 00000000000000..2dffc8c26ca7c2
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManagerRegistry.java
@@ -0,0 +1,38 @@
+/**
+ * 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.uimanager;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Class that stores the mapping between native view name used in JS and the corresponding instance
+ * of {@link ViewManager}.
+ */
+/* package */ class ViewManagerRegistry {
+
+ private final Map mViewManagers = new HashMap<>();
+
+ public ViewManagerRegistry(List viewManagerList) {
+ for (ViewManager viewManager : viewManagerList) {
+ mViewManagers.put(viewManager.getName(), viewManager);
+ }
+ }
+
+ /* package */ ViewManager get(String className) {
+ ViewManager viewManager = mViewManagers.get(className);
+ if (viewManager != null) {
+ return viewManager;
+ } else {
+ throw new IllegalViewOperationException("No ViewManager defined for class " + className);
+ }
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.java
new file mode 100644
index 00000000000000..525a1d89261cd2
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.java
@@ -0,0 +1,112 @@
+/**
+ * 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.uimanager;
+
+import java.util.Arrays;
+import java.util.HashSet;
+
+import com.facebook.csslayout.Spacing;
+import com.facebook.react.common.SetBuilder;
+
+/**
+ * Keys for props that need to be shared across multiple classes.
+ */
+public class ViewProps {
+
+ public static final String VIEW_CLASS_NAME = "RCTView";
+
+ // Layout only (only affect positions of children, causes no drawing)
+ // !!! Keep in sync with LAYOUT_ONLY_PROPS below
+ public static final String ALIGN_ITEMS = "alignItems";
+ public static final String ALIGN_SELF = "alignSelf";
+ public static final String BOTTOM = "bottom";
+ public static final String COLLAPSABLE = "collapsable";
+ public static final String FLEX = "flex";
+ public static final String FLEX_DIRECTION = "flexDirection";
+ public static final String FLEX_WRAP = "flexWrap";
+ public static final String HEIGHT = "height";
+ public static final String JUSTIFY_CONTENT = "justifyContent";
+ public static final String LEFT = "left";
+ public static final String[] MARGINS = {
+ "margin", "marginVertical", "marginHorizontal", "marginLeft", "marginRight", "marginTop",
+ "marginBottom"
+ };
+ public static final String[] PADDINGS = {
+ "padding", "paddingVertical", "paddingHorizontal", "paddingLeft", "paddingRight",
+ "paddingTop", "paddingBottom"
+ };
+ public static final String POSITION = "position";
+ public static final String RIGHT = "right";
+ public static final String TOP = "top";
+ public static final String WIDTH = "width";
+
+ // Props that affect more than just layout
+ public static final String ENABLED = "enabled";
+ public static final String BACKGROUND_COLOR = "backgroundColor";
+ public static final String COLOR = "color";
+ public static final String FONT_SIZE = "fontSize";
+ public static final String FONT_WEIGHT = "fontWeight";
+ public static final String FONT_STYLE = "fontStyle";
+ public static final String FONT_FAMILY = "fontFamily";
+ public static final String LINE_HEIGHT = "lineHeight";
+ public static final String NEEDS_OFFSCREEN_ALPHA_COMPOSITING = "needsOffscreenAlphaCompositing";
+ public static final String NUMBER_OF_LINES = "numberOfLines";
+ public static final String ON = "on";
+ public static final String RESIZE_MODE = "resizeMode";
+ public static final String TEXT_ALIGN = "textAlign";
+ public static final String BORDER_WIDTH = "borderWidth";
+ public static final String BORDER_LEFT_WIDTH = "borderLeftWidth";
+ public static final String BORDER_TOP_WIDTH = "borderTopWidth";
+ public static final String BORDER_RIGHT_WIDTH = "borderRightWidth";
+ public static final String BORDER_BOTTOM_WIDTH = "borderBottomWidth";
+ public static final int[] BORDER_SPACING_TYPES = {
+ Spacing.ALL, Spacing.LEFT, Spacing.RIGHT, Spacing.TOP, Spacing.BOTTOM
+ };
+ public static final String[] BORDER_WIDTHS = {
+ BORDER_WIDTH, BORDER_LEFT_WIDTH, BORDER_RIGHT_WIDTH, BORDER_TOP_WIDTH, BORDER_BOTTOM_WIDTH,
+ };
+ public static final int[] PADDING_MARGIN_SPACING_TYPES = {
+ Spacing.ALL, Spacing.VERTICAL, Spacing.HORIZONTAL, Spacing.LEFT, Spacing.RIGHT, Spacing.TOP,
+ Spacing.BOTTOM
+ };
+
+ private static final HashSet LAYOUT_ONLY_PROPS = createLayoutOnlyPropsMap();
+
+ private static HashSet createLayoutOnlyPropsMap() {
+ HashSet layoutOnlyProps = SetBuilder.newHashSet();
+ layoutOnlyProps.addAll(
+ Arrays.asList(
+ ALIGN_SELF,
+ ALIGN_ITEMS,
+ BOTTOM,
+ COLLAPSABLE,
+ FLEX,
+ FLEX_DIRECTION,
+ FLEX_WRAP,
+ HEIGHT,
+ JUSTIFY_CONTENT,
+ LEFT,
+ POSITION,
+ RIGHT,
+ TOP,
+ WIDTH));
+ for (int i = 0; i < MARGINS.length; i++) {
+ layoutOnlyProps.add(MARGINS[i]);
+ }
+ for (int i = 0; i < PADDINGS.length; i++) {
+ layoutOnlyProps.add(PADDINGS[i]);
+ }
+ return layoutOnlyProps;
+ }
+
+ public static boolean isLayoutOnly(String prop) {
+ return LAYOUT_ONLY_PROPS.contains(prop);
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/debug/DebugComponentOwnershipModule.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/debug/DebugComponentOwnershipModule.java
new file mode 100644
index 00000000000000..859c74c99b49b8
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/debug/DebugComponentOwnershipModule.java
@@ -0,0 +1,100 @@
+/**
+ * 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.catalyst.uimanager.debug;
+
+import javax.annotation.Nullable;
+
+import android.util.SparseArray;
+
+import com.facebook.infer.annotation.Assertions;
+import com.facebook.react.bridge.JSApplicationCausedNativeException;
+import com.facebook.react.bridge.JavaScriptModule;
+import com.facebook.react.bridge.ReactApplicationContext;
+import com.facebook.react.bridge.ReactContextBaseJavaModule;
+import com.facebook.react.bridge.ReactMethod;
+import com.facebook.react.bridge.ReadableArray;
+
+/**
+ * Native module that can asynchronously request the owners hierarchy of a react tag.
+ *
+ * Example returned owner hierarchy: ['RootView', 'Dialog', 'TitleView', 'Text']
+ */
+public class DebugComponentOwnershipModule extends ReactContextBaseJavaModule {
+
+ public interface RCTDebugComponentOwnership extends JavaScriptModule {
+
+ void getOwnerHierarchy(int requestID, int tag);
+ }
+
+ /**
+ * Callback for when we receive the ownership hierarchy in native code.
+ *
+ * NB: {@link #onOwnerHierarchyLoaded} will be called on the native modules thread!
+ */
+ public static interface OwnerHierarchyCallback {
+
+ void onOwnerHierarchyLoaded(int tag, @Nullable ReadableArray owners);
+ }
+
+ private final SparseArray mRequestIdToCallback = new SparseArray<>();
+
+ private @Nullable RCTDebugComponentOwnership mRCTDebugComponentOwnership;
+ private int mNextRequestId = 0;
+
+ public DebugComponentOwnershipModule(ReactApplicationContext reactContext) {
+ super(reactContext);
+ }
+
+ @Override
+ public void initialize() {
+ super.initialize();
+ mRCTDebugComponentOwnership = getReactApplicationContext().
+ getJSModule(RCTDebugComponentOwnership.class);
+ }
+
+ @Override
+ public void onCatalystInstanceDestroy() {
+ super.onCatalystInstanceDestroy();
+ mRCTDebugComponentOwnership = null;
+ }
+
+ @ReactMethod
+ public synchronized void receiveOwnershipHierarchy(
+ int requestId,
+ int tag,
+ @Nullable ReadableArray owners) {
+ OwnerHierarchyCallback callback = mRequestIdToCallback.get(requestId);
+ if (callback == null) {
+ throw new JSApplicationCausedNativeException(
+ "Got receiveOwnershipHierarchy for invalid request id: " + requestId);
+ }
+ mRequestIdToCallback.delete(requestId);
+ callback.onOwnerHierarchyLoaded(tag, owners);
+ }
+
+ /**
+ * Request to receive the component hierarchy for a particular tag.
+ *
+ * Example returned owner hierarchy: ['RootView', 'Dialog', 'TitleView', 'Text']
+ *
+ * NB: The callback provided will be invoked on the native modules thread!
+ */
+ public synchronized void loadComponentOwnerHierarchy(int tag, OwnerHierarchyCallback callback) {
+ int requestId = mNextRequestId;
+ mNextRequestId++;
+ mRequestIdToCallback.put(requestId, callback);
+ Assertions.assertNotNull(mRCTDebugComponentOwnership).getOwnerHierarchy(requestId, tag);
+ }
+
+ @Override
+ public String getName() {
+ return "DebugComponentOwnershipModule";
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/debug/NotThreadSafeUiManagerDebugListener.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/debug/NotThreadSafeUiManagerDebugListener.java
new file mode 100644
index 00000000000000..1f4a5690b558bf
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/debug/NotThreadSafeUiManagerDebugListener.java
@@ -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.uimanager.debug;
+
+import com.facebook.react.uimanager.UIManagerModule;
+
+/**
+ * A listener that is notified about {@link UIManagerModule} events. This listener should only be
+ * used for debug purposes and should not affect application state.
+ *
+ * NB: while onViewHierarchyUpdateFinished will always be called from the UI thread, there are no
+ * guarantees what thread onViewHierarchyUpdateEnqueued is called on.
+ */
+public interface NotThreadSafeUiManagerDebugListener {
+
+ /**
+ * Called when {@link UIManagerModule} enqueues a UI batch to be dispatched to the main thread.
+ */
+ void onViewHierarchyUpdateEnqueued();
+
+ /**
+ * Called from the main thread after a UI batch has been applied to all root views.
+ */
+ void onViewHierarchyUpdateFinished();
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/Event.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/Event.java
new file mode 100644
index 00000000000000..505fea794297bb
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/Event.java
@@ -0,0 +1,83 @@
+/**
+ * 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.uimanager.events;
+
+/**
+ * A UI event that can be dispatched to JS.
+ */
+public abstract class Event {
+
+ private final int mViewTag;
+ private final long mTimestampMs;
+
+ protected Event(int viewTag, long timestampMs) {
+ mViewTag = viewTag;
+ mTimestampMs = timestampMs;
+ }
+
+ /**
+ * @return the view id for the view that generated this event
+ */
+ public final int getViewTag() {
+ return mViewTag;
+ }
+
+ /**
+ * @return the time at which the event happened in the {@link android.os.SystemClock#uptimeMillis}
+ * base.
+ */
+ public final long getTimestampMs() {
+ return mTimestampMs;
+ }
+
+ /**
+ * @return false if this Event can *never* be coalesced
+ */
+ public boolean canCoalesce() {
+ return true;
+ }
+
+ /**
+ * Given two events, coalesce them into a single event that will be sent to JS instead of two
+ * separate events. By default, just chooses the one the is more recent.
+ *
+ * Two events will only ever try to be coalesced if they have the same event name, view id, and
+ * coalescing key.
+ */
+ public T coalesce(T otherEvent) {
+ return (T) (getTimestampMs() > otherEvent.getTimestampMs() ? this : otherEvent);
+ }
+
+ /**
+ * @return a key used to determine which other events of this type this event can be coalesced
+ * with. For example, touch move events should only be coalesced within a single gesture so a
+ * coalescing key there would be the unique gesture id.
+ */
+ public short getCoalescingKey() {
+ return 0;
+ }
+
+ /**
+ * Called when the EventDispatcher is done with an event, either because it was dispatched or
+ * because it was coalesced with another Event.
+ */
+ public void dispose() {
+ }
+
+ /**
+ * @return the name of this event as registered in JS
+ */
+ public abstract String getEventName();
+
+ /**
+ * Dispatch this event to JS using the given event emitter.
+ */
+ public abstract void dispatch(RCTEventEmitter rctEventEmitter);
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/EventDispatcher.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/EventDispatcher.java
new file mode 100644
index 00000000000000..aa3315c169ba43
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/EventDispatcher.java
@@ -0,0 +1,302 @@
+/**
+ * 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.uimanager.events;
+
+import javax.annotation.Nullable;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Map;
+
+import android.util.LongSparseArray;
+import android.view.Choreographer;
+
+import com.facebook.infer.annotation.Assertions;
+import com.facebook.react.bridge.LifecycleEventListener;
+import com.facebook.react.bridge.ReactApplicationContext;
+import com.facebook.react.bridge.UiThreadUtil;
+import com.facebook.react.common.MapBuilder;
+import com.facebook.react.uimanager.ReactChoreographer;
+import com.facebook.systrace.Systrace;
+
+/**
+ * Class responsible for dispatching UI events to JS. The main purpose of this class is to act as an
+ * intermediary between UI code generating events and JS, making sure we don't send more events than
+ * JS can process.
+ *
+ * To use it, create a subclass of {@link Event} and call {@link #dispatchEvent(Event)} whenever
+ * there's a UI event to dispatch.
+ *
+ * This class works by installing a Choreographer frame callback on the main thread. This callback
+ * then enqueues a runnable on the JS thread (if one is not already pending) that is responsible for
+ * actually dispatch events to JS. This implementation depends on the properties that
+ * 1) FrameCallbacks run after UI events have been processed in Choreographer.java
+ * 2) when we enqueue a runnable on the JS queue thread, it won't be called until after any
+ * previously enqueued JS jobs have finished processing
+ *
+ * If JS is taking a long time processing events, then the UI events generated on the UI thread can
+ * be coalesced into fewer events so that when the runnable runs, we don't overload JS with a ton
+ * of events and make it get even farther behind.
+ *
+ * Ideally, we don't need this and JS is fast enough to process all the events each frame, but bad
+ * things happen, including load on CPUs from the system, and we should handle this case well.
+ *
+ * == Event Cookies ==
+ *
+ * An event cookie is made up of the event type id, view tag, and a custom coalescing key. Only
+ * Events that have the same cookie can be coalesced.
+ *
+ * Event Cookie Composition:
+ * VIEW_TAG_MASK = 0x00000000ffffffff
+ * EVENT_TYPE_ID_MASK = 0x0000ffff00000000
+ * COALESCING_KEY_MASK = 0xffff000000000000
+ */
+public class EventDispatcher implements LifecycleEventListener {
+
+ private static final Comparator EVENT_COMPARATOR = new Comparator() {
+ @Override
+ public int compare(Event lhs, Event rhs) {
+ if (lhs == null && rhs == null) {
+ return 0;
+ }
+ if (lhs == null) {
+ return -1;
+ }
+ if (rhs == null) {
+ return 1;
+ }
+
+ long diff = lhs.getTimestampMs() - rhs.getTimestampMs();
+ if (diff == 0) {
+ return 0;
+ } else if (diff < 0) {
+ return -1;
+ } else {
+ return 1;
+ }
+ }
+ };
+
+ private final Object mEventsStagingLock = new Object();
+ private final Object mEventsToDispatchLock = new Object();
+ private final ReactApplicationContext mReactContext;
+ private final LongSparseArray mEventCookieToLastEventIdx = new LongSparseArray<>();
+ private final Map mEventNameToEventId = MapBuilder.newHashMap();
+ private final DispatchEventsRunnable mDispatchEventsRunnable = new DispatchEventsRunnable();
+ private final ArrayList mEventStaging = new ArrayList<>();
+
+ private Event[] mEventsToDispatch = new Event[16];
+ private int mEventsToDispatchSize = 0;
+ private @Nullable RCTEventEmitter mRCTEventEmitter;
+ private volatile @Nullable ScheduleDispatchFrameCallback mCurrentFrameCallback;
+ private short mNextEventTypeId = 0;
+ private volatile boolean mHasDispatchScheduled = false;
+
+ public EventDispatcher(ReactApplicationContext reactContext) {
+ mReactContext = reactContext;
+ mReactContext.addLifecycleEventListener(this);
+ }
+
+ /**
+ * Sends the given Event to JS, coalescing eligible events if JS is backed up.
+ */
+ public void dispatchEvent(Event event) {
+ synchronized (mEventsStagingLock) {
+ mEventStaging.add(event);
+ }
+ }
+
+ @Override
+ public void onHostResume() {
+ UiThreadUtil.assertOnUiThread();
+ Assertions.assumeCondition(mCurrentFrameCallback == null);
+
+ if (mRCTEventEmitter == null) {
+ mRCTEventEmitter = mReactContext.getJSModule(RCTEventEmitter.class);
+ }
+
+ mCurrentFrameCallback = new ScheduleDispatchFrameCallback();
+ ReactChoreographer.getInstance()
+ .postFrameCallback(ReactChoreographer.CallbackType.TIMERS_EVENTS, mCurrentFrameCallback);
+ }
+
+ @Override
+ public void onHostPause() {
+ clearFrameCallback();
+ }
+
+ @Override
+ public void onHostDestroy() {
+ clearFrameCallback();
+ }
+
+ public void onCatalystInstanceDestroyed() {
+ clearFrameCallback();
+ }
+
+ private void clearFrameCallback() {
+ UiThreadUtil.assertOnUiThread();
+ if (mCurrentFrameCallback != null) {
+ mCurrentFrameCallback.stop();
+ mCurrentFrameCallback = null;
+ }
+ }
+
+ /**
+ * We use a staging data structure so that all UI events generated in a single frame are
+ * dispatched at once. Otherwise, a JS runnable enqueued in a previous frame could run while the
+ * UI thread is in the process of adding UI events and we might incorrectly send one event this
+ * frame and another from this frame during the next.
+ */
+ private void moveStagedEventsToDispatchQueue() {
+ synchronized (mEventsStagingLock) {
+ synchronized (mEventsToDispatchLock) {
+ for (int i = 0; i < mEventStaging.size(); i++) {
+ Event event = mEventStaging.get(i);
+
+ if (!event.canCoalesce()) {
+ addEventToEventsToDispatch(event);
+ continue;
+ }
+
+ long eventCookie = getEventCookie(
+ event.getViewTag(),
+ event.getEventName(),
+ event.getCoalescingKey());
+
+ Event eventToAdd = null;
+ Event eventToDispose = null;
+ Integer lastEventIdx = mEventCookieToLastEventIdx.get(eventCookie);
+
+ if (lastEventIdx == null) {
+ eventToAdd = event;
+ mEventCookieToLastEventIdx.put(eventCookie, mEventsToDispatchSize);
+ } else {
+ Event lastEvent = mEventsToDispatch[lastEventIdx];
+ Event coalescedEvent = event.coalesce(lastEvent);
+ if (coalescedEvent != lastEvent) {
+ eventToAdd = coalescedEvent;
+ mEventCookieToLastEventIdx.put(eventCookie, mEventsToDispatchSize);
+ eventToDispose = lastEvent;
+ mEventsToDispatch[lastEventIdx] = null;
+ } else {
+ eventToDispose = event;
+ }
+ }
+
+ if (eventToAdd != null) {
+ addEventToEventsToDispatch(eventToAdd);
+ }
+ if (eventToDispose != null) {
+ eventToDispose.dispose();
+ }
+ }
+ }
+ mEventStaging.clear();
+ }
+ }
+
+ private long getEventCookie(int viewTag, String eventName, short coalescingKey) {
+ short eventTypeId;
+ Short eventIdObj = mEventNameToEventId.get(eventName);
+ if (eventIdObj != null) {
+ eventTypeId = eventIdObj;
+ } else {
+ eventTypeId = mNextEventTypeId++;
+ mEventNameToEventId.put(eventName, eventTypeId);
+ }
+ return getEventCookie(viewTag, eventTypeId, coalescingKey);
+ }
+
+ private static long getEventCookie(int viewTag, short eventTypeId, short coalescingKey) {
+ return viewTag |
+ (((long) eventTypeId) & 0xffff) << 32 |
+ (((long) coalescingKey) & 0xffff) << 48;
+ }
+
+ private class ScheduleDispatchFrameCallback implements Choreographer.FrameCallback {
+
+ private boolean mShouldStop = false;
+
+ @Override
+ public void doFrame(long frameTimeNanos) {
+ UiThreadUtil.assertOnUiThread();
+
+ if (mShouldStop) {
+ return;
+ }
+
+ Systrace.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "ScheduleDispatchFrameCallback");
+ try {
+ moveStagedEventsToDispatchQueue();
+
+ if (!mHasDispatchScheduled) {
+ mHasDispatchScheduled = true;
+ mReactContext.runOnJSQueueThread(mDispatchEventsRunnable);
+ }
+
+ ReactChoreographer.getInstance()
+ .postFrameCallback(ReactChoreographer.CallbackType.TIMERS_EVENTS, this);
+ } finally {
+ Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE);
+ }
+ }
+
+ public void stop() {
+ mShouldStop = true;
+ }
+ }
+
+ private class DispatchEventsRunnable implements Runnable {
+
+ @Override
+ public void run() {
+ Systrace.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "DispatchEventsRunnable");
+ try {
+ mHasDispatchScheduled = false;
+ Assertions.assertNotNull(mRCTEventEmitter);
+ synchronized (mEventsToDispatchLock) {
+ // We avoid allocating an array and iterator, and "sorting" if we don't need to.
+ // This occurs when the size of mEventsToDispatch is zero or one.
+ if (mEventsToDispatchSize > 1) {
+ Arrays.sort(mEventsToDispatch, 0, mEventsToDispatchSize, EVENT_COMPARATOR);
+ }
+ for (int eventIdx = 0; eventIdx < mEventsToDispatchSize; eventIdx++) {
+ Event event = mEventsToDispatch[eventIdx];
+ // Event can be null if it has been coalesced into another event.
+ if (event == null) {
+ continue;
+ }
+ event.dispatch(mRCTEventEmitter);
+ event.dispose();
+ }
+ clearEventsToDispatch();
+ mEventCookieToLastEventIdx.clear();
+ }
+ } finally {
+ Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE);
+ }
+ }
+ }
+
+ private void addEventToEventsToDispatch(Event event) {
+ if (mEventsToDispatchSize == mEventsToDispatch.length) {
+ mEventsToDispatch = Arrays.copyOf(mEventsToDispatch, 2 * mEventsToDispatch.length);
+ }
+ mEventsToDispatch[mEventsToDispatchSize++] = event;
+ }
+
+ private void clearEventsToDispatch() {
+ Arrays.fill(mEventsToDispatch, 0, mEventsToDispatchSize, null);
+ mEventsToDispatchSize = 0;
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/NativeGestureUtil.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/NativeGestureUtil.java
new file mode 100644
index 00000000000000..6ef3011b2a8add
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/NativeGestureUtil.java
@@ -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.uimanager.events;
+
+import android.view.MotionEvent;
+import android.view.View;
+
+import com.facebook.react.uimanager.RootViewUtil;
+
+/**
+ * Utilities for native Views that interpret native gestures (e.g. ScrollView, ViewPager, etc.).
+ */
+public class NativeGestureUtil {
+
+ /**
+ * Helper method that should be called when a native view starts a native gesture (e.g. a native
+ * ScrollView takes control of a gesture stream and starts scrolling). This will handle
+ * dispatching the appropriate events to JS to make sure the gesture in JS is canceled.
+ *
+ * @param view the View starting the native gesture
+ * @param event the MotionEvent that caused the gesture to be started
+ */
+ public static void notifyNativeGestureStarted(View view, MotionEvent event) {
+ RootViewUtil.getRootView(view).onChildStartedNativeGesture(event);
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/RCTEventEmitter.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/RCTEventEmitter.java
new file mode 100644
index 00000000000000..4fa6f36770cb5e
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/RCTEventEmitter.java
@@ -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.uimanager.events;
+
+import javax.annotation.Nullable;
+
+import com.facebook.react.bridge.JavaScriptModule;
+import com.facebook.react.bridge.WritableArray;
+import com.facebook.react.bridge.WritableMap;
+
+public interface RCTEventEmitter extends JavaScriptModule {
+ public void receiveEvent(int targetTag, String eventName, @Nullable WritableMap event);
+ public void receiveTouches(
+ String eventName,
+ WritableArray touches,
+ WritableArray changedIndices);
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchEvent.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchEvent.java
new file mode 100644
index 00000000000000..62e52373fb0b69
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchEvent.java
@@ -0,0 +1,98 @@
+/**
+ * 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.uimanager.events;
+
+import android.view.MotionEvent;
+
+/**
+ * An event representing the start, end or movement of a touch. Corresponds to a single
+ * {@link android.view.MotionEvent}.
+ *
+ * TouchEvent coalescing can happen for move events if two move events have the same target view and
+ * coalescing key. See {@link TouchEventCoalescingKeyHelper} for more information about how these
+ * coalescing keys are determined.
+ */
+public class TouchEvent extends Event {
+
+ private final MotionEvent mMotionEvent;
+ private final TouchEventType mTouchEventType;
+ private final short mCoalescingKey;
+
+ public TouchEvent(int viewTag, TouchEventType touchEventType, MotionEvent motionEventToCopy) {
+ super(viewTag, motionEventToCopy.getEventTime());
+ mTouchEventType = touchEventType;
+ mMotionEvent = MotionEvent.obtain(motionEventToCopy);
+
+ short coalescingKey = 0;
+ int action = (mMotionEvent.getAction() & MotionEvent.ACTION_MASK);
+ switch (action) {
+ case MotionEvent.ACTION_DOWN:
+ TouchEventCoalescingKeyHelper.addCoalescingKey(mMotionEvent.getDownTime());
+ break;
+ case MotionEvent.ACTION_UP:
+ TouchEventCoalescingKeyHelper.removeCoalescingKey(mMotionEvent.getDownTime());
+ break;
+ case MotionEvent.ACTION_POINTER_DOWN:
+ case MotionEvent.ACTION_POINTER_UP:
+ TouchEventCoalescingKeyHelper.incrementCoalescingKey(mMotionEvent.getDownTime());
+ break;
+ case MotionEvent.ACTION_MOVE:
+ coalescingKey = TouchEventCoalescingKeyHelper.getCoalescingKey(mMotionEvent.getDownTime());
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ TouchEventCoalescingKeyHelper.removeCoalescingKey(mMotionEvent.getDownTime());
+ break;
+ default:
+ throw new RuntimeException("Unhandled MotionEvent action: " + action);
+ }
+ mCoalescingKey = coalescingKey;
+ }
+
+ @Override
+ public String getEventName() {
+ return mTouchEventType.getJSEventName();
+ }
+
+ @Override
+ public boolean canCoalesce() {
+ // We can coalesce move events but not start/end events. Coalescing move events should probably
+ // append historical move data like MotionEvent batching does. This is left as an exercise for
+ // the reader.
+ switch (mTouchEventType) {
+ case START:
+ case END:
+ case CANCEL:
+ return false;
+ case MOVE:
+ return true;
+ default:
+ throw new RuntimeException("Unknown touch event type: " + mTouchEventType);
+ }
+ }
+
+ @Override
+ public short getCoalescingKey() {
+ return mCoalescingKey;
+ }
+
+ @Override
+ public void dispatch(RCTEventEmitter rctEventEmitter) {
+ TouchesHelper.sendTouchEvent(
+ rctEventEmitter,
+ mTouchEventType,
+ getViewTag(),
+ mMotionEvent);
+ }
+
+ @Override
+ public void dispose() {
+ mMotionEvent.recycle();
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchEventCoalescingKeyHelper.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchEventCoalescingKeyHelper.java
new file mode 100644
index 00000000000000..e5783e3c5d5935
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchEventCoalescingKeyHelper.java
@@ -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.uimanager.events;
+
+import android.util.SparseIntArray;
+
+/**
+ * Utility for determining coalescing keys for TouchEvents. To preserve proper ordering of events,
+ * move events should only be coalesced if there has been no up/down event between them (this
+ * basically only applies to multitouch since for single touches an up would signal the end of the
+ * gesture). To illustrate to kind of coalescing we want, imagine we are coalescing the following
+ * touch stream:
+ *
+ * (U = finger up, D = finger down, M = move)
+ * D MMMMM D MMMMMMMMMMMMMM U MMMMM D MMMMMM U U
+ *
+ * We want to make sure to coalesce this as
+ *
+ * D M D M U M D U U
+ *
+ * and *not*
+ *
+ * D D U M D U U
+ *
+ * To accomplish this, this class provides a way to initialize a coalescing key for a gesture and
+ * then increment it for every pointer up/down that occurs during that single gesture.
+ *
+ * We identify a single gesture based on {@link android.view.MotionEvent#getDownTime()} which will
+ * stay constant for a given set of related touches on a single view.
+ *
+ * NB: even though down time is a long, we cast as an int using the least significant bits as the
+ * identifier. In practice, we will not be coalescing over a time range where the most significant
+ * bits of that time range matter. This would require a gesture that lasts Integer.MAX_VALUE * 2 ms,
+ * or ~48 days.
+ *
+ * NB: we assume two gestures cannot begin at the same time.
+ *
+ * NB: this class should only be used from the UI thread.
+ */
+public class TouchEventCoalescingKeyHelper {
+
+ private static final SparseIntArray sDownTimeToCoalescingKey = new SparseIntArray();
+
+ /**
+ * Starts tracking a new coalescing key corresponding to the gesture with this down time.
+ */
+ public static void addCoalescingKey(long downTime) {
+ sDownTimeToCoalescingKey.put((int) downTime, 0);
+ }
+
+ /**
+ * Increments the coalescing key corresponding to the gesture with this down time.
+ */
+ public static void incrementCoalescingKey(long downTime) {
+ int currentValue = sDownTimeToCoalescingKey.get((int) downTime, -1);
+ if (currentValue == -1) {
+ throw new RuntimeException("Tried to increment non-existent cookie");
+ }
+ sDownTimeToCoalescingKey.put((int) downTime, currentValue + 1);
+ }
+
+ /**
+ * Gets the coalescing key corresponding to the gesture with this down time.
+ */
+ public static short getCoalescingKey(long downTime) {
+ int currentValue = sDownTimeToCoalescingKey.get((int) downTime, -1);
+ if (currentValue == -1) {
+ throw new RuntimeException("Tried to get non-existent cookie");
+ }
+ return ((short) (0xffff & currentValue));
+ }
+
+ /**
+ * Stops tracking a new coalescing key corresponding to the gesture with this down time.
+ */
+ public static void removeCoalescingKey(long downTime) {
+ sDownTimeToCoalescingKey.delete((int) downTime);
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchEventType.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchEventType.java
new file mode 100644
index 00000000000000..36f32966114e90
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchEventType.java
@@ -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.uimanager.events;
+
+/**
+ * Touch event types that JS module RCTEventEmitter can understand
+ */
+public enum TouchEventType {
+ START("topTouchStart"),
+ END("topTouchEnd"),
+ MOVE("topTouchMove"),
+ CANCEL("topTouchCancel");
+
+ private final String mJSEventName;
+
+ TouchEventType(String jsEventName) {
+ mJSEventName = jsEventName;
+ }
+
+ public String getJSEventName() {
+ return mJSEventName;
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchesHelper.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchesHelper.java
new file mode 100644
index 00000000000000..56c6ff0ada2f33
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchesHelper.java
@@ -0,0 +1,99 @@
+/**
+ * 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.uimanager.events;
+
+import android.view.MotionEvent;
+
+import com.facebook.react.bridge.Arguments;
+import com.facebook.react.bridge.WritableArray;
+import com.facebook.react.bridge.WritableMap;
+import com.facebook.react.uimanager.PixelUtil;
+
+/**
+ * Class responsible for generating catalyst touch events based on android {@link MotionEvent}.
+ */
+/*package*/ class TouchesHelper {
+
+ private static final String PAGE_X_KEY = "pageX";
+ private static final String PAGE_Y_KEY = "pageY";
+ private static final String TARGET_KEY = "target";
+ private static final String TIMESTAMP_KEY = "timeStamp";
+ private static final String POINTER_IDENTIFIER_KEY = "identifier";
+
+ // TODO(7351435): remove when we standardize touchEvent payload, since iOS uses locationXYZ but
+ // Android uses pageXYZ. As a temporary solution, Android currently sends both.
+ private static final String LOCATION_X_KEY = "locationX";
+ private static final String LOCATION_Y_KEY = "locationY";
+
+ /**
+ * Creates catalyst pointers array in format that is expected by RCTEventEmitter JS module from
+ * given {@param event} instance. This method use {@param reactTarget} parameter to set as a
+ * target view id associated with current gesture.
+ */
+ private static WritableArray createsPointersArray(int reactTarget, MotionEvent event) {
+ WritableArray touches = Arguments.createArray();
+
+ // Calculate raw-to-relative offset as getRawX() and getRawY() can only return values for the
+ // pointer at index 0. We use those value to calculate "raw" coordinates for other pointers
+ float offsetX = event.getRawX() - event.getX();
+ float offsetY = event.getRawY() - event.getY();
+
+ for (int index = 0; index < event.getPointerCount(); index++) {
+ WritableMap touch = Arguments.createMap();
+ touch.putDouble(PAGE_X_KEY, PixelUtil.toDIPFromPixel(event.getX(index) + offsetX));
+ touch.putDouble(PAGE_Y_KEY, PixelUtil.toDIPFromPixel(event.getY(index) + offsetY));
+ touch.putDouble(LOCATION_X_KEY, PixelUtil.toDIPFromPixel(event.getX(index)));
+ touch.putDouble(LOCATION_Y_KEY, PixelUtil.toDIPFromPixel(event.getY(index)));
+ touch.putInt(TARGET_KEY, reactTarget);
+ touch.putDouble(TIMESTAMP_KEY, event.getEventTime());
+ touch.putDouble(POINTER_IDENTIFIER_KEY, event.getPointerId(index));
+ touches.pushMap(touch);
+ }
+
+ return touches;
+ }
+
+ /**
+ * Generate and send touch event to RCTEventEmitter JS module associated with the given
+ * {@param context}. Touch event can encode multiple concurrent touches (pointers).
+ *
+ * @param rctEventEmitter Event emitter used to execute JS module call
+ * @param type type of the touch event (see {@link TouchEventType})
+ * @param reactTarget target view react id associated with this gesture
+ * @param androidMotionEvent native touch event to read pointers count and coordinates from
+ */
+ public static void sendTouchEvent(
+ RCTEventEmitter rctEventEmitter,
+ TouchEventType type,
+ int reactTarget,
+ MotionEvent androidMotionEvent) {
+
+ WritableArray pointers = createsPointersArray(reactTarget, androidMotionEvent);
+
+ // For START and END events send only index of the pointer that is associated with that event
+ // For MOVE and CANCEL events 'changedIndices' array should contain all the pointers indices
+ WritableArray changedIndices = Arguments.createArray();
+ if (type == TouchEventType.MOVE || type == TouchEventType.CANCEL) {
+ for (int i = 0; i < androidMotionEvent.getPointerCount(); i++) {
+ changedIndices.pushInt(i);
+ }
+ } else if (type == TouchEventType.START || type == TouchEventType.END) {
+ changedIndices.pushInt(androidMotionEvent.getActionIndex());
+ } else {
+ throw new RuntimeException("Unknown touch type: " + type);
+ }
+
+ rctEventEmitter.receiveTouches(
+ type.getJSEventName(),
+ pointers,
+ changedIndices);
+ }
+
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/drawer/ReactDrawerLayout.java b/ReactAndroid/src/main/java/com/facebook/react/views/drawer/ReactDrawerLayout.java
new file mode 100644
index 00000000000000..9d0d32405f9ab8
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/drawer/ReactDrawerLayout.java
@@ -0,0 +1,73 @@
+/**
+ * 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.views.drawer;
+
+import android.support.v4.widget.DrawerLayout;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.View;
+
+import com.facebook.react.bridge.ReactContext;
+import com.facebook.react.uimanager.PixelUtil;
+import com.facebook.react.uimanager.events.NativeGestureUtil;
+
+/**
+ * Wrapper view for {@link DrawerLayout}. It manages the properties that can be set on the drawer
+ * and contains some ReactNative-specific functionality.
+ */
+/* package */ class ReactDrawerLayout extends DrawerLayout {
+
+ public static final int DEFAULT_DRAWER_WIDTH = LayoutParams.MATCH_PARENT;
+ private int mDrawerPosition = Gravity.START;
+ private int mDrawerWidth = DEFAULT_DRAWER_WIDTH;
+
+ public ReactDrawerLayout(ReactContext reactContext) {
+ super(reactContext);
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ if (super.onInterceptTouchEvent(ev)) {
+ NativeGestureUtil.notifyNativeGestureStarted(this, ev);
+ return true;
+ }
+ return false;
+ }
+
+ /* package */ void openDrawer() {
+ openDrawer(mDrawerPosition);
+ }
+
+ /* package */ void closeDrawer() {
+ closeDrawer(mDrawerPosition);
+ }
+
+ /* package */ void setDrawerPosition(int drawerPosition) {
+ mDrawerPosition = drawerPosition;
+ setDrawerProperties();
+ }
+
+ /* package */ void setDrawerWidth(int drawerWidth) {
+ mDrawerWidth = (int) PixelUtil.toPixelFromDIP((float) drawerWidth);
+ setDrawerProperties();
+ }
+
+ // Sets the properties of the drawer, after the navigationView has been set.
+ /* package */ void setDrawerProperties() {
+ if (this.getChildCount() == 2) {
+ View drawerView = this.getChildAt(1);
+ LayoutParams layoutParams = (LayoutParams) drawerView.getLayoutParams();
+ layoutParams.gravity = mDrawerPosition;
+ layoutParams.width = mDrawerWidth;
+ drawerView.setLayoutParams(layoutParams);
+ drawerView.setClickable(true);
+ }
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/drawer/ReactDrawerLayoutManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/drawer/ReactDrawerLayoutManager.java
new file mode 100644
index 00000000000000..eb07905a7eeaf3
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/drawer/ReactDrawerLayoutManager.java
@@ -0,0 +1,182 @@
+/**
+ * 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.views.drawer;
+
+import javax.annotation.Nullable;
+
+import java.util.Map;
+
+import android.os.SystemClock;
+import android.support.v4.widget.DrawerLayout;
+import android.view.Gravity;
+import android.view.View;
+
+import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
+import com.facebook.react.bridge.ReadableArray;
+import com.facebook.react.common.MapBuilder;
+import com.facebook.react.uimanager.CatalystStylesDiffMap;
+import com.facebook.react.uimanager.ThemedReactContext;
+import com.facebook.react.uimanager.UIManagerModule;
+import com.facebook.react.uimanager.UIProp;
+import com.facebook.react.uimanager.ViewGroupManager;
+import com.facebook.react.uimanager.events.EventDispatcher;
+import com.facebook.react.views.drawer.events.DrawerClosedEvent;
+import com.facebook.react.views.drawer.events.DrawerOpenedEvent;
+import com.facebook.react.views.drawer.events.DrawerSlideEvent;
+import com.facebook.react.views.drawer.events.DrawerStateChangedEvent;
+
+/**
+ * View Manager for {@link ReactDrawerLayout} components.
+ */
+public class ReactDrawerLayoutManager extends ViewGroupManager {
+
+ private static final String REACT_CLASS = "AndroidDrawerLayout";
+
+ public static final int OPEN_DRAWER = 1;
+ public static final int CLOSE_DRAWER = 2;
+
+ @UIProp(UIProp.Type.NUMBER)
+ public static final String PROP_DRAWER_POSITION = "drawerPosition";
+ @UIProp(UIProp.Type.NUMBER)
+ public static final String PROP_DRAWER_WIDTH = "drawerWidth";
+
+ @Override
+ public String getName() {
+ return REACT_CLASS;
+ }
+
+ @Override
+ protected void addEventEmitters(ThemedReactContext reactContext, ReactDrawerLayout view) {
+ view.setDrawerListener(
+ new DrawerEventEmitter(
+ view,
+ reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher()));
+ }
+
+ @Override
+ protected ReactDrawerLayout createViewInstance(ThemedReactContext context) {
+ return new ReactDrawerLayout(context);
+ }
+
+ @Override
+ public void updateView(ReactDrawerLayout view, CatalystStylesDiffMap props) {
+ super.updateView(view, props);
+
+ if (props.hasKey(PROP_DRAWER_POSITION)) {
+ int drawerPosition = props.getInt(PROP_DRAWER_POSITION, -1);
+ if (Gravity.START == drawerPosition || Gravity.END == drawerPosition) {
+ view.setDrawerPosition(drawerPosition);
+ } else {
+ throw new JSApplicationIllegalArgumentException("Unknown drawerPosition " + drawerPosition);
+ }
+ }
+
+ if (props.hasKey(PROP_DRAWER_WIDTH)) {
+ view.setDrawerWidth(props.getInt(PROP_DRAWER_WIDTH, ReactDrawerLayout.DEFAULT_DRAWER_WIDTH));
+ }
+ }
+
+ @Override
+ public boolean needsCustomLayoutForChildren() {
+ // Return true, since DrawerLayout will lay out it's own children.
+ return true;
+ }
+
+ @Override
+ public @Nullable Map getCommandsMap() {
+ return MapBuilder.of("openDrawer", OPEN_DRAWER, "closeDrawer", CLOSE_DRAWER);
+ }
+
+ @Override
+ public void receiveCommand(
+ ReactDrawerLayout root,
+ int commandId,
+ @Nullable ReadableArray args) {
+ switch (commandId) {
+ case OPEN_DRAWER:
+ root.openDrawer();
+ break;
+ case CLOSE_DRAWER:
+ root.closeDrawer();
+ break;
+ }
+ }
+
+ @Override
+ public @Nullable Map getExportedViewConstants() {
+ return MapBuilder.of(
+ "DrawerPosition",
+ MapBuilder.of("Left", Gravity.START, "Right", Gravity.END));
+ }
+
+ @Override
+ public @Nullable Map getExportedCustomDirectEventTypeConstants() {
+ return MapBuilder.of(
+ DrawerSlideEvent.EVENT_NAME, MapBuilder.of("registrationName", "onDrawerSlide"),
+ DrawerOpenedEvent.EVENT_NAME, MapBuilder.of("registrationName", "onDrawerOpen"),
+ DrawerClosedEvent.EVENT_NAME, MapBuilder.of("registrationName", "onDrawerClose"),
+ DrawerStateChangedEvent.EVENT_NAME, MapBuilder.of(
+ "registrationName", "onDrawerStateChanged"));
+ }
+
+ /**
+ * This method is overridden because of two reasons:
+ * 1. A drawer must have exactly two children
+ * 2. The second child that is added, is the navigationView, which gets panned from the side.
+ */
+ @Override
+ public void addView(ReactDrawerLayout parent, View child, int index) {
+ if (getChildCount(parent) >= 2) {
+ throw new
+ JSApplicationIllegalArgumentException("The Drawer cannot have more than two children");
+ }
+ if (index != 0 && index != 1) {
+ throw new JSApplicationIllegalArgumentException(
+ "The only valid indices for drawer's child are 0 or 1. Got " + index + " instead.");
+ }
+ parent.addView(child, index);
+ parent.setDrawerProperties();
+ }
+
+ public static class DrawerEventEmitter implements DrawerLayout.DrawerListener {
+
+ private final DrawerLayout mDrawerLayout;
+ private final EventDispatcher mEventDispatcher;
+
+ public DrawerEventEmitter(DrawerLayout drawerLayout, EventDispatcher eventDispatcher) {
+ mDrawerLayout = drawerLayout;
+ mEventDispatcher = eventDispatcher;
+ }
+
+ @Override
+ public void onDrawerSlide(View view, float v) {
+ mEventDispatcher.dispatchEvent(
+ new DrawerSlideEvent(mDrawerLayout.getId(), SystemClock.uptimeMillis(), v));
+ }
+
+ @Override
+ public void onDrawerOpened(View view) {
+ mEventDispatcher.dispatchEvent(
+ new DrawerOpenedEvent(mDrawerLayout.getId(), SystemClock.uptimeMillis()));
+ }
+
+ @Override
+ public void onDrawerClosed(View view) {
+ mEventDispatcher.dispatchEvent(
+ new DrawerClosedEvent(mDrawerLayout.getId(), SystemClock.uptimeMillis()));
+ }
+
+ @Override
+ public void onDrawerStateChanged(int i) {
+ mEventDispatcher.dispatchEvent(
+ new DrawerStateChangedEvent(mDrawerLayout.getId(), SystemClock.uptimeMillis(), i));
+ }
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/drawer/events/DrawerClosedEvent.java b/ReactAndroid/src/main/java/com/facebook/react/views/drawer/events/DrawerClosedEvent.java
new file mode 100644
index 00000000000000..83bc9f50f8387b
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/drawer/events/DrawerClosedEvent.java
@@ -0,0 +1,40 @@
+/**
+ * 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.views.drawer.events;
+
+import com.facebook.react.bridge.Arguments;
+import com.facebook.react.uimanager.events.Event;
+import com.facebook.react.uimanager.events.RCTEventEmitter;
+
+public class DrawerClosedEvent extends Event {
+
+ public static final String EVENT_NAME = "topDrawerClosed";
+
+ public DrawerClosedEvent(int viewId, long timestampMs) {
+ super(viewId, timestampMs);
+ }
+
+ @Override
+ public String getEventName() {
+ return EVENT_NAME;
+ }
+
+ @Override
+ public short getCoalescingKey() {
+ // All events for a given view can be coalesced.
+ return 0;
+ }
+
+ @Override
+ public void dispatch(RCTEventEmitter rctEventEmitter) {
+ rctEventEmitter.receiveEvent(getViewTag(), getEventName(), Arguments.createMap());
+ }
+
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/drawer/events/DrawerOpenedEvent.java b/ReactAndroid/src/main/java/com/facebook/react/views/drawer/events/DrawerOpenedEvent.java
new file mode 100644
index 00000000000000..916c301c96570e
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/drawer/events/DrawerOpenedEvent.java
@@ -0,0 +1,40 @@
+/**
+ * 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.views.drawer.events;
+
+import com.facebook.react.bridge.Arguments;
+import com.facebook.react.uimanager.events.Event;
+import com.facebook.react.uimanager.events.RCTEventEmitter;
+
+public class DrawerOpenedEvent extends Event {
+
+ public static final String EVENT_NAME = "topDrawerOpened";
+
+ public DrawerOpenedEvent(int viewId, long timestampMs) {
+ super(viewId, timestampMs);
+ }
+
+ @Override
+ public String getEventName() {
+ return EVENT_NAME;
+ }
+
+ @Override
+ public short getCoalescingKey() {
+ // All events for a given view can be coalesced.
+ return 0;
+ }
+
+ @Override
+ public void dispatch(RCTEventEmitter rctEventEmitter) {
+ rctEventEmitter.receiveEvent(getViewTag(), getEventName(), Arguments.createMap());
+ }
+
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/drawer/events/DrawerSlideEvent.java b/ReactAndroid/src/main/java/com/facebook/react/views/drawer/events/DrawerSlideEvent.java
new file mode 100644
index 00000000000000..b35bbc8d0fbbad
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/drawer/events/DrawerSlideEvent.java
@@ -0,0 +1,56 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+package com.facebook.react.views.drawer.events;
+
+import com.facebook.react.bridge.Arguments;
+import com.facebook.react.bridge.WritableMap;
+import com.facebook.react.uimanager.events.Event;
+import com.facebook.react.uimanager.events.RCTEventEmitter;
+
+/**
+ * Event emitted by a DrawerLayout as it is being moved open/closed.
+ */
+public class DrawerSlideEvent extends Event {
+
+ public static final String EVENT_NAME = "topDrawerSlide";
+
+ private final float mOffset;
+
+ public DrawerSlideEvent(int viewId, long timestampMs, float offset) {
+ super(viewId, timestampMs);
+ mOffset = offset;
+ }
+
+ public float getOffset() {
+ return mOffset;
+ }
+
+ @Override
+ public String getEventName() {
+ return EVENT_NAME;
+ }
+
+ @Override
+ public short getCoalescingKey() {
+ // All slide events for a given view can be coalesced.
+ return 0;
+ }
+
+ @Override
+ public void dispatch(RCTEventEmitter rctEventEmitter) {
+ rctEventEmitter.receiveEvent(getViewTag(), getEventName(), serializeEventData());
+ }
+
+ private WritableMap serializeEventData() {
+ WritableMap eventData = Arguments.createMap();
+ eventData.putDouble("offset", getOffset());
+ return eventData;
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/drawer/events/DrawerStateChangedEvent.java b/ReactAndroid/src/main/java/com/facebook/react/views/drawer/events/DrawerStateChangedEvent.java
new file mode 100644
index 00000000000000..dc6c9cd9c3410b
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/drawer/events/DrawerStateChangedEvent.java
@@ -0,0 +1,53 @@
+/**
+ * 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.views.drawer.events;
+
+import com.facebook.react.bridge.Arguments;
+import com.facebook.react.bridge.WritableMap;
+import com.facebook.react.uimanager.events.Event;
+import com.facebook.react.uimanager.events.RCTEventEmitter;
+
+public class DrawerStateChangedEvent extends Event {
+
+ public static final String EVENT_NAME = "topDrawerStateChanged";
+
+ private final int mDrawerState;
+
+ public DrawerStateChangedEvent(int viewId, long timestampMs, int drawerState) {
+ super(viewId, timestampMs);
+ mDrawerState = drawerState;
+ }
+
+ public int getDrawerState() {
+ return mDrawerState;
+ }
+
+ @Override
+ public String getEventName() {
+ return EVENT_NAME;
+ }
+
+ @Override
+ public short getCoalescingKey() {
+ // All events for a given view can be coalesced.
+ return 0;
+ }
+
+ @Override
+ public void dispatch(RCTEventEmitter rctEventEmitter) {
+ rctEventEmitter.receiveEvent(getViewTag(), getEventName(), serializeEventData());
+ }
+
+ private WritableMap serializeEventData() {
+ WritableMap eventData = Arguments.createMap();
+ eventData.putDouble("drawerState", getDrawerState());
+ return eventData;
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/image/ImageResizeMode.java b/ReactAndroid/src/main/java/com/facebook/react/views/image/ImageResizeMode.java
new file mode 100644
index 00000000000000..fd7a6f67f2b789
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/image/ImageResizeMode.java
@@ -0,0 +1,51 @@
+/**
+ * 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.views.image;
+
+import javax.annotation.Nullable;
+
+import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
+import com.facebook.drawee.drawable.ScalingUtils;
+
+/**
+ * Converts JS resize modes into Android-specific scale type.
+ */
+public class ImageResizeMode {
+
+ /**
+ * Converts JS resize modes into {@code ScalingUtils.ScaleType}.
+ * See {@code ImageResizeMode.js}.
+ */
+ public static ScalingUtils.ScaleType toScaleType(@Nullable String resizeModeValue) {
+ if ("contain".equals(resizeModeValue)) {
+ return ScalingUtils.ScaleType.CENTER_INSIDE;
+ }
+ if ("cover".equals(resizeModeValue)) {
+ return ScalingUtils.ScaleType.CENTER_CROP;
+ }
+ if ("stretch".equals(resizeModeValue)) {
+ return ScalingUtils.ScaleType.FIT_XY;
+ }
+ if (resizeModeValue == null) {
+ // Use the default. Never use null.
+ return defaultValue();
+ }
+ throw new JSApplicationIllegalArgumentException(
+ "Invalid resize mode: '" + resizeModeValue + "'");
+ }
+
+ /**
+ * This is the default as per web and iOS.
+ * We want to be consistent across platforms.
+ */
+ public static ScalingUtils.ScaleType defaultValue() {
+ return ScalingUtils.ScaleType.CENTER_CROP;
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageManager.java
new file mode 100644
index 00000000000000..b7cabec7857d02
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageManager.java
@@ -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.views.image;
+
+import javax.annotation.Nullable;
+
+import com.facebook.drawee.backends.pipeline.Fresco;
+import com.facebook.drawee.controller.AbstractDraweeControllerBuilder;
+import com.facebook.react.uimanager.CSSColorUtil;
+import com.facebook.react.uimanager.CatalystStylesDiffMap;
+import com.facebook.react.uimanager.SimpleViewManager;
+import com.facebook.react.uimanager.ThemedReactContext;
+import com.facebook.react.uimanager.UIProp;
+import com.facebook.react.uimanager.ViewProps;
+
+public class ReactImageManager extends SimpleViewManager {
+
+ public static final String REACT_CLASS = "RCTImageView";
+
+ @Override
+ public String getName() {
+ return REACT_CLASS;
+ }
+
+ // In JS this is Image.props.source.uri
+ @UIProp(UIProp.Type.STRING)
+ public static final String PROP_SRC = "src";
+ @UIProp(UIProp.Type.NUMBER)
+ public static final String PROP_BORDER_RADIUS = "borderRadius";
+ @UIProp(UIProp.Type.STRING)
+ public static final String PROP_RESIZE_MODE = ViewProps.RESIZE_MODE;
+ private static final String PROP_TINT_COLOR = "tintColor";
+
+ private final @Nullable AbstractDraweeControllerBuilder mDraweeControllerBuilder;
+ private final @Nullable Object mCallerContext;
+
+ public ReactImageManager(
+ AbstractDraweeControllerBuilder draweeControllerBuilder,
+ Object callerContext) {
+ mDraweeControllerBuilder = draweeControllerBuilder;
+ mCallerContext = callerContext;
+ }
+
+ public ReactImageManager() {
+ mDraweeControllerBuilder = null;
+ mCallerContext = null;
+ }
+
+ @Override
+ public ReactImageView createViewInstance(ThemedReactContext context) {
+ return new ReactImageView(
+ context,
+ mDraweeControllerBuilder == null ?
+ Fresco.newDraweeControllerBuilder() : mDraweeControllerBuilder,
+ mCallerContext);
+ }
+
+ @Override
+ public void updateView(final ReactImageView view, final CatalystStylesDiffMap props) {
+ super.updateView(view, props);
+
+ if (props.hasKey(PROP_RESIZE_MODE)) {
+ view.setScaleType(ImageResizeMode.toScaleType(props.getString(PROP_RESIZE_MODE)));
+ }
+ if (props.hasKey(PROP_SRC)) {
+ view.setSource(props.getString(PROP_SRC));
+ }
+ if (props.hasKey(PROP_BORDER_RADIUS)) {
+ view.setBorderRadius(props.getFloat(PROP_BORDER_RADIUS, 0.0f));
+ }
+ if (props.hasKey(PROP_TINT_COLOR)) {
+ String tintColorString = props.getString(PROP_TINT_COLOR);
+ if (tintColorString == null) {
+ view.clearColorFilter();
+ } else {
+ view.setColorFilter(CSSColorUtil.getColor(tintColorString));
+ }
+ }
+ view.maybeUpdateView();
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.java b/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.java
new file mode 100644
index 00000000000000..c8ba6c06143ed5
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.java
@@ -0,0 +1,259 @@
+/**
+ * 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.views.image;
+
+import javax.annotation.Nullable;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapShader;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.Shader;
+import android.net.Uri;
+
+import com.facebook.drawee.controller.AbstractDraweeControllerBuilder;
+import com.facebook.drawee.controller.ControllerListener;
+import com.facebook.react.uimanager.PixelUtil;
+import com.facebook.common.util.UriUtil;
+import com.facebook.drawee.backends.pipeline.Fresco;
+import com.facebook.drawee.backends.pipeline.PipelineDraweeControllerBuilder;
+import com.facebook.drawee.drawable.ScalingUtils;
+import com.facebook.drawee.generic.GenericDraweeHierarchy;
+import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
+import com.facebook.drawee.generic.RoundingParams;
+import com.facebook.drawee.interfaces.DraweeController;
+import com.facebook.drawee.view.GenericDraweeView;
+import com.facebook.imagepipeline.common.ResizeOptions;
+import com.facebook.imagepipeline.request.BasePostprocessor;
+import com.facebook.imagepipeline.request.ImageRequest;
+import com.facebook.imagepipeline.request.ImageRequestBuilder;
+import com.facebook.imagepipeline.request.Postprocessor;
+
+/**
+ * Wrapper class around Fresco's GenericDraweeView, enabling persisting props across multiple view
+ * update and consistent processing of both static and network images.
+ */
+public class ReactImageView extends GenericDraweeView {
+
+ private static final int REMOTE_IMAGE_FADE_DURATION_MS = 300;
+ public static final String TAG = ReactImageView.class.getSimpleName();
+
+ /*
+ * Implementation note re rounded corners:
+ *
+ * Fresco's built-in rounded corners only work for 'cover' resize mode -
+ * this is a limitation in Android itself. Fresco has a workaround for this, but
+ * it requires knowing the background color.
+ *
+ * So for the other modes, we use a postprocessor.
+ * Because the postprocessor uses a modified bitmap, that would just get cropped in
+ * 'cover' mode, so we fall back to Fresco's normal implementation.
+ */
+ private static final Matrix sMatrix = new Matrix();
+ private static final Matrix sInverse = new Matrix();
+
+ private class RoundedCornerPostprocessor extends BasePostprocessor {
+
+ float getRadius(Bitmap source) {
+ ScalingUtils.getTransform(
+ sMatrix,
+ new Rect(0, 0, source.getWidth(), source.getHeight()),
+ source.getWidth(),
+ source.getHeight(),
+ 0.0f,
+ 0.0f,
+ mScaleType);
+ sMatrix.invert(sInverse);
+ return sInverse.mapRadius(mBorderRadius);
+ }
+
+ @Override
+ public void process(Bitmap output, Bitmap source) {
+ output.setHasAlpha(true);
+ if (mBorderRadius < 0.01f) {
+ super.process(output, source);
+ return;
+ }
+ Paint paint = new Paint();
+ paint.setAntiAlias(true);
+ paint.setShader(new BitmapShader(source, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP));
+ Canvas canvas = new Canvas(output);
+ float radius = getRadius(source);
+ canvas.drawRoundRect(
+ new RectF(0, 0, source.getWidth(), source.getHeight()),
+ radius,
+ radius,
+ paint);
+ }
+ }
+
+ private @Nullable Uri mUri;
+ private float mBorderRadius;
+ private ScalingUtils.ScaleType mScaleType;
+ private boolean mIsDirty;
+ private boolean mIsLocalImage;
+ private final AbstractDraweeControllerBuilder mDraweeControllerBuilder;
+ private final RoundedCornerPostprocessor mRoundedCornerPostprocessor;
+ private final @Nullable Object mCallerContext;
+ private @Nullable ControllerListener mControllerListener;
+ private int mImageFadeDuration = -1;
+
+ // We can't specify rounding in XML, so have to do so here
+ private static GenericDraweeHierarchy buildHierarchy(Context context) {
+ return new GenericDraweeHierarchyBuilder(context.getResources())
+ .setRoundingParams(RoundingParams.fromCornersRadius(0))
+ .build();
+ }
+
+ public ReactImageView(
+ Context context,
+ AbstractDraweeControllerBuilder draweeControllerBuilder,
+ @Nullable Object callerContext) {
+ super(context, buildHierarchy(context));
+ mScaleType = ImageResizeMode.defaultValue();
+ mDraweeControllerBuilder = draweeControllerBuilder;
+ mRoundedCornerPostprocessor = new RoundedCornerPostprocessor();
+ mCallerContext = callerContext;
+ }
+
+ public void setBorderRadius(float borderRadius) {
+ mBorderRadius = PixelUtil.toPixelFromDIP(borderRadius);
+ mIsDirty = true;
+ }
+
+ public void setScaleType(ScalingUtils.ScaleType scaleType) {
+ mScaleType = scaleType;
+ mIsDirty = true;
+ }
+
+ public void setSource(@Nullable String source) {
+ mUri = null;
+ if (source != null) {
+ try {
+ mUri = Uri.parse(source);
+ // Verify scheme is set, so that relative uri (used by static resources) are not handled.
+ if (mUri.getScheme() == null) {
+ mUri = null;
+ }
+ } catch (Exception e) {
+ // ignore malformed uri, then attempt to extract resource ID.
+ }
+ if (mUri == null) {
+ mUri = getResourceDrawableUri(getContext(), source);
+ mIsLocalImage = true;
+ } else {
+ mIsLocalImage = false;
+ }
+ }
+ mIsDirty = true;
+ }
+
+ public void maybeUpdateView() {
+ if (!mIsDirty) {
+ return;
+ }
+
+ boolean doResize = shouldResize(mUri);
+ if (doResize && (getWidth() <= 0 || getHeight() <=0)) {
+ // If need a resize and the size is not yet set, wait until the layout pass provides one
+ return;
+ }
+
+ GenericDraweeHierarchy hierarchy = getHierarchy();
+ hierarchy.setActualImageScaleType(mScaleType);
+
+ boolean usePostprocessorScaling =
+ mScaleType != ScalingUtils.ScaleType.CENTER_CROP &&
+ mScaleType != ScalingUtils.ScaleType.FOCUS_CROP;
+ float hierarchyRadius = usePostprocessorScaling ? 0 : mBorderRadius;
+
+ RoundingParams roundingParams = hierarchy.getRoundingParams();
+ roundingParams.setCornersRadius(hierarchyRadius);
+ hierarchy.setRoundingParams(roundingParams);
+ hierarchy.setFadeDuration(mImageFadeDuration >= 0
+ ? mImageFadeDuration
+ : mIsLocalImage ? 0 : REMOTE_IMAGE_FADE_DURATION_MS);
+
+ Postprocessor postprocessor = usePostprocessorScaling ? mRoundedCornerPostprocessor : null;
+
+ ResizeOptions resizeOptions = doResize ? new ResizeOptions(getWidth(), getHeight()) : null;
+
+ ImageRequest imageRequest = ImageRequestBuilder.newBuilderWithSource(mUri)
+ .setPostprocessor(postprocessor)
+ .setResizeOptions(resizeOptions)
+ .build();
+
+ DraweeController draweeController = mDraweeControllerBuilder
+ .reset()
+ .setCallerContext(mCallerContext)
+ .setOldController(getController())
+ .setImageRequest(imageRequest)
+ .setControllerListener(mControllerListener)
+ .build();
+ setController(draweeController);
+ mIsDirty = false;
+ }
+
+ // VisibleForTesting
+ public void setControllerListener(ControllerListener controllerListener) {
+ mControllerListener = controllerListener;
+ mIsDirty = true;
+ maybeUpdateView();
+ }
+
+ // VisibleForTesting
+ public void setImageFadeDuration(int imageFadeDuration) {
+ mImageFadeDuration = imageFadeDuration;
+ mIsDirty = true;
+ maybeUpdateView();
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+ if (w > 0 && h > 0) {
+ maybeUpdateView();
+ }
+ }
+
+ /**
+ * ReactImageViews only render a single image.
+ */
+ @Override
+ public boolean hasOverlappingRendering() {
+ return false;
+ }
+
+ private static boolean shouldResize(@Nullable Uri uri) {
+ // Resizing is inferior to scaling. See http://frescolib.org/docs/resizing-rotating.html#_
+ // We resize here only for images likely to be from the device's camera, where the app developer
+ // has no control over the original size
+ return uri != null && (UriUtil.isLocalContentUri(uri) || UriUtil.isLocalFileUri(uri));
+ }
+
+ private static @Nullable Uri getResourceDrawableUri(Context context, @Nullable String name) {
+ if (name == null || name.isEmpty()) {
+ return null;
+ }
+ name = name.toLowerCase().replace("-", "_");
+ int resId = context.getResources().getIdentifier(
+ name,
+ "drawable",
+ context.getPackageName());
+ return new Uri.Builder()
+ .scheme(UriUtil.LOCAL_RESOURCE_SCHEME)
+ .path(String.valueOf(resId))
+ .build();
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/progressbar/ProgressBarShadowNode.java b/ReactAndroid/src/main/java/com/facebook/react/views/progressbar/ProgressBarShadowNode.java
new file mode 100644
index 00000000000000..b6d9f315ff2199
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/progressbar/ProgressBarShadowNode.java
@@ -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.views.progressbar;
+
+import javax.annotation.Nullable;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import android.util.SparseIntArray;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ProgressBar;
+
+import com.facebook.csslayout.CSSNode;
+import com.facebook.csslayout.MeasureOutput;
+import com.facebook.react.uimanager.CatalystStylesDiffMap;
+import com.facebook.react.uimanager.ReactShadowNode;
+import com.facebook.infer.annotation.Assertions;
+
+/**
+ * Node responsible for holding the style of the ProgressBar, see under
+ * {@link android.R.attr.progressBarStyle} for possible styles. ReactProgressBarViewManager
+ * manages how this style is applied to the ProgressBar.
+ */
+public class ProgressBarShadowNode extends ReactShadowNode implements CSSNode.MeasureFunction {
+
+ private @Nullable String style;
+
+ private final SparseIntArray mHeight = new SparseIntArray();
+ private final SparseIntArray mWidth = new SparseIntArray();
+ private final Set mMeasured = new HashSet<>();
+
+ public ProgressBarShadowNode() {
+ setMeasureFunction(this);
+ }
+
+ public @Nullable String getStyle() {
+ return style;
+ }
+
+ public void setStyle(String style) {
+ this.style = style;
+ }
+
+ @Override
+ public void measure(CSSNode node, float width, MeasureOutput measureOutput) {
+ final int style = ReactProgressBarViewManager.getStyleFromString(getStyle());
+ if (!mMeasured.contains(style)) {
+ ProgressBar progressBar = new ProgressBar(getThemedContext(), null, style);
+ final int spec = View.MeasureSpec.makeMeasureSpec(
+ ViewGroup.LayoutParams.WRAP_CONTENT,
+ View.MeasureSpec.UNSPECIFIED);
+ progressBar.measure(spec, spec);
+ mHeight.put(style, progressBar.getMeasuredHeight());
+ mWidth.put(style, progressBar.getMeasuredWidth());
+ mMeasured.add(style);
+ }
+
+ measureOutput.height = mHeight.get(style);
+ measureOutput.width = mWidth.get(style);
+ }
+
+ @Override
+ public void updateProperties(CatalystStylesDiffMap styles) {
+ super.updateProperties(styles);
+
+ if (styles.hasKey(ReactProgressBarViewManager.PROP_STYLE)) {
+ String style = styles.getString(ReactProgressBarViewManager.PROP_STYLE);
+ Assertions.assertNotNull(
+ style,
+ "style property should always be set for the progress bar component");
+ // TODO(7255944): Validate progressbar style attribute
+ setStyle(style);
+ } else {
+ setStyle(ReactProgressBarViewManager.DEFAULT_STYLE);
+ }
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/progressbar/ReactProgressBarViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/progressbar/ReactProgressBarViewManager.java
new file mode 100644
index 00000000000000..296e9d40054ac3
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/progressbar/ReactProgressBarViewManager.java
@@ -0,0 +1,93 @@
+/**
+ * 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.views.progressbar;
+
+import javax.annotation.Nullable;
+
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.ProgressBar;
+
+import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
+import com.facebook.react.uimanager.BaseViewPropertyApplicator;
+import com.facebook.react.uimanager.CatalystStylesDiffMap;
+import com.facebook.react.uimanager.ThemedReactContext;
+import com.facebook.react.uimanager.UIProp;
+import com.facebook.react.uimanager.ViewManager;
+
+/**
+ * Manages instances of ProgressBar. ProgressBar is wrapped in a FrameLayout because the style of
+ * the ProgressBar can only be set in the constructor; whenever the style of a ProgressBar changes,
+ * we have to drop the existing ProgressBar (if there is one) and create a new one with the style
+ * given.
+ */
+public class ReactProgressBarViewManager extends ViewManager {
+
+ @UIProp(UIProp.Type.STRING) public static final String PROP_STYLE = "styleAttr";
+
+ /* package */ static final String REACT_CLASS = "AndroidProgressBar";
+ /* package */ static final String DEFAULT_STYLE = "Large";
+
+ @Override
+ public String getName() {
+ return REACT_CLASS;
+ }
+
+ @Override
+ protected FrameLayout createViewInstance(ThemedReactContext context) {
+ return new FrameLayout(context);
+ }
+
+ @Override
+ public void updateView(FrameLayout view, CatalystStylesDiffMap props) {
+ BaseViewPropertyApplicator.applyCommonViewProperties(view, props);
+ if (props.hasKey(PROP_STYLE)) {
+ final int style = getStyleFromString(props.getString(PROP_STYLE));
+ view.removeAllViews();
+ view.addView(
+ new ProgressBar(view.getContext(), null, style),
+ new ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT));
+ }
+ }
+
+ @Override
+ public ProgressBarShadowNode createCSSNodeInstance() {
+ return new ProgressBarShadowNode();
+ }
+
+ @Override
+ public void updateExtraData(FrameLayout root, Object extraData) {
+ // do nothing
+ }
+
+ /* package */ static int getStyleFromString(@Nullable String styleStr) {
+ if (styleStr == null) {
+ throw new JSApplicationIllegalArgumentException(
+ "ProgressBar needs to have a style, null received");
+ } else if (styleStr.equals("Horizontal")) {
+ return android.R.attr.progressBarStyleHorizontal;
+ } else if (styleStr.equals("Small")) {
+ return android.R.attr.progressBarStyleSmall;
+ } else if (styleStr.equals("Large")) {
+ return android.R.attr.progressBarStyleLarge;
+ } else if (styleStr.equals("Inverse")) {
+ return android.R.attr.progressBarStyleInverse;
+ } else if (styleStr.equals("SmallInverse")) {
+ return android.R.attr.progressBarStyleSmallInverse;
+ } else if (styleStr.equals("LargeInverse")) {
+ return android.R.attr.progressBarStyleLargeInverse;
+ } else {
+ throw new JSApplicationIllegalArgumentException("Unknown ProgressBar style: " + styleStr);
+ }
+ }
+
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/OnScrollDispatchHelper.java b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/OnScrollDispatchHelper.java
new file mode 100644
index 00000000000000..a9078c420a578a
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/OnScrollDispatchHelper.java
@@ -0,0 +1,44 @@
+/**
+ * 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.views.scroll;
+
+import android.os.SystemClock;
+
+/**
+ * Android has a bug where onScrollChanged is called twice per frame with the same params during
+ * flings. We hack around that here by trying to detect that duplicate call and not dispatch it. See
+ * https://code.google.com/p/android/issues/detail?id=39473
+ */
+public class OnScrollDispatchHelper {
+
+ private static final int MIN_EVENT_SEPARATION_MS = 10;
+
+ private int mPrevX = Integer.MIN_VALUE;
+ private int mPrevY = Integer.MIN_VALUE;
+ private long mLastScrollEventTimeMs = -(MIN_EVENT_SEPARATION_MS + 1);
+
+ /**
+ * Call from a ScrollView in onScrollChanged, returns true if this onScrollChanged is legit (not a
+ * duplicate) and should be dispatched.
+ */
+ public boolean onScrollChanged(int x, int y) {
+ long eventTime = SystemClock.uptimeMillis();
+ boolean shouldDispatch =
+ eventTime - mLastScrollEventTimeMs > MIN_EVENT_SEPARATION_MS ||
+ mPrevX != x ||
+ mPrevY != y;
+
+ mLastScrollEventTimeMs = eventTime;
+ mPrevX = x;
+ mPrevY = y;
+
+ return shouldDispatch;
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.java b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.java
new file mode 100644
index 00000000000000..ec528cedf287b9
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.java
@@ -0,0 +1,63 @@
+/**
+ * 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.views.scroll;
+
+import android.content.Context;
+import android.view.MotionEvent;
+import android.widget.HorizontalScrollView;
+
+import com.facebook.react.uimanager.MeasureSpecAssertions;
+import com.facebook.react.uimanager.events.NativeGestureUtil;
+
+/**
+ * Similar to {@link ReactScrollView} but only supports horizontal scrolling.
+ */
+public class ReactHorizontalScrollView extends HorizontalScrollView {
+
+ private final OnScrollDispatchHelper mOnScrollDispatchHelper = new OnScrollDispatchHelper();
+
+ public ReactHorizontalScrollView(Context context) {
+ super(context);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ MeasureSpecAssertions.assertExplicitMeasureSpec(widthMeasureSpec, heightMeasureSpec);
+
+ setMeasuredDimension(
+ MeasureSpec.getSize(widthMeasureSpec),
+ MeasureSpec.getSize(heightMeasureSpec));
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ // Call with the present values in order to re-layout if necessary
+ scrollTo(getScrollX(), getScrollY());
+ }
+
+ @Override
+ protected void onScrollChanged(int x, int y, int oldX, int oldY) {
+ super.onScrollChanged(x, y, oldX, oldY);
+
+ if (mOnScrollDispatchHelper.onScrollChanged(x, y)) {
+ ReactScrollViewHelper.emitScrollEvent(this, x, y);
+ }
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ if (super.onInterceptTouchEvent(ev)) {
+ NativeGestureUtil.notifyNativeGestureStarted(this, ev);
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollViewManager.java
new file mode 100644
index 00000000000000..aa8c784ccf1a87
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollViewManager.java
@@ -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.views.scroll;
+
+import javax.annotation.Nullable;
+
+import com.facebook.react.bridge.ReadableArray;
+import com.facebook.react.uimanager.ThemedReactContext;
+import com.facebook.react.uimanager.ViewGroupManager;
+
+/**
+ * View manager for {@link ReactHorizontalScrollView} components.
+ *
+ * Note that {@link ReactScrollView} and {@link ReactHorizontalScrollView} are exposed to JS
+ * as a single ScrollView component, configured via the {@code horizontal} boolean property.
+ */
+public class ReactHorizontalScrollViewManager
+ extends ViewGroupManager
+ implements ReactScrollViewCommandHelper.ScrollCommandHandler {
+
+ private static final String REACT_CLASS = "AndroidHorizontalScrollView";
+
+ @Override
+ public String getName() {
+ return REACT_CLASS;
+ }
+
+ @Override
+ public ReactHorizontalScrollView createViewInstance(ThemedReactContext context) {
+ return new ReactHorizontalScrollView(context);
+ }
+
+ @Override
+ public void receiveCommand(
+ ReactHorizontalScrollView scrollView,
+ int commandId,
+ @Nullable ReadableArray args) {
+ ReactScrollViewCommandHelper.receiveCommand(this, scrollView, commandId, args);
+ }
+
+ @Override
+ public void scrollTo(
+ ReactHorizontalScrollView scrollView,
+ ReactScrollViewCommandHelper.ScrollToCommandData data) {
+ scrollView.smoothScrollTo(data.mDestX, data.mDestY);
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java
new file mode 100644
index 00000000000000..cc8098efcd1606
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java
@@ -0,0 +1,123 @@
+/**
+ * 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.views.scroll;
+
+import javax.annotation.Nullable;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.ScrollView;
+
+import com.facebook.react.uimanager.MeasureSpecAssertions;
+import com.facebook.react.uimanager.events.NativeGestureUtil;
+import com.facebook.react.views.view.ReactClippingViewGroup;
+import com.facebook.react.views.view.ReactClippingViewGroupHelper;
+import com.facebook.infer.annotation.Assertions;
+
+/**
+ * A simple subclass of ScrollView that doesn't dispatch measure and layout to its children and has
+ * a scroll listener to send scroll events to JS.
+ *
+ * ReactScrollView only supports vertical scrolling. For horizontal scrolling,
+ * use {@link ReactHorizontalScrollView}.
+ */
+public class ReactScrollView extends ScrollView implements ReactClippingViewGroup {
+
+ private final OnScrollDispatchHelper mOnScrollDispatchHelper = new OnScrollDispatchHelper();
+
+ private boolean mRemoveClippedSubviews;
+ private @Nullable Rect mClippingRect;
+
+ public ReactScrollView(Context context) {
+ super(context);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ MeasureSpecAssertions.assertExplicitMeasureSpec(widthMeasureSpec, heightMeasureSpec);
+
+ setMeasuredDimension(
+ MeasureSpec.getSize(widthMeasureSpec),
+ MeasureSpec.getSize(heightMeasureSpec));
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ // Call with the present values in order to re-layout if necessary
+ scrollTo(getScrollX(), getScrollY());
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+ if (mRemoveClippedSubviews) {
+ updateClippingRect();
+ }
+ }
+
+ @Override
+ protected void onScrollChanged(int x, int y, int oldX, int oldY) {
+ super.onScrollChanged(x, y, oldX, oldY);
+
+ if (mOnScrollDispatchHelper.onScrollChanged(x, y)) {
+ if (mRemoveClippedSubviews) {
+ updateClippingRect();
+ }
+
+ ReactScrollViewHelper.emitScrollEvent(this, x, y);
+ }
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ if (super.onInterceptTouchEvent(ev)) {
+ NativeGestureUtil.notifyNativeGestureStarted(this, ev);
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public void setRemoveClippedSubviews(boolean removeClippedSubviews) {
+ if (removeClippedSubviews && mClippingRect == null) {
+ mClippingRect = new Rect();
+ }
+ mRemoveClippedSubviews = removeClippedSubviews;
+ updateClippingRect();
+ }
+
+ @Override
+ public boolean getRemoveClippedSubviews() {
+ return mRemoveClippedSubviews;
+ }
+
+ @Override
+ public void updateClippingRect() {
+ if (!mRemoveClippedSubviews) {
+ return;
+ }
+
+ Assertions.assertNotNull(mClippingRect);
+
+ ReactClippingViewGroupHelper.calculateClippingRect(this, mClippingRect);
+ View contentView = getChildAt(0);
+ if (contentView instanceof ReactClippingViewGroup) {
+ ((ReactClippingViewGroup) contentView).updateClippingRect();
+ }
+ }
+
+ @Override
+ public void getClippingRect(Rect outClippingRect) {
+ outClippingRect.set(Assertions.assertNotNull(mClippingRect));
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewCommandHelper.java b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewCommandHelper.java
new file mode 100644
index 00000000000000..840fde9ddecb51
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewCommandHelper.java
@@ -0,0 +1,68 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+package com.facebook.react.views.scroll;
+
+import javax.annotation.Nullable;
+
+import java.util.Map;
+
+import com.facebook.react.bridge.ReadableArray;
+import com.facebook.react.uimanager.PixelUtil;
+import com.facebook.infer.annotation.Assertions;
+import com.facebook.react.common.MapBuilder;
+
+/**
+ * Helper for view managers to handle commands like 'scrollTo'.
+ * Shared by {@link ReactScrollViewManager} and {@link ReactHorizontalScrollViewManager}.
+ */
+public class ReactScrollViewCommandHelper {
+
+ public static final int COMMAND_SCROLL_TO = 1;
+
+ public interface ScrollCommandHandler {
+ void scrollTo(T scrollView, ScrollToCommandData data);
+ }
+
+ public static class ScrollToCommandData {
+
+ public final int mDestX, mDestY;
+
+ ScrollToCommandData(int destX, int destY) {
+ mDestX = destX;
+ mDestY = destY;
+ }
+ }
+
+ public static Map getCommandsMap() {
+ return MapBuilder.of("scrollTo", COMMAND_SCROLL_TO);
+ }
+
+ public static void receiveCommand(
+ ScrollCommandHandler viewManager,
+ T scrollView,
+ int commandType,
+ @Nullable ReadableArray args) {
+ Assertions.assertNotNull(viewManager);
+ Assertions.assertNotNull(scrollView);
+ Assertions.assertNotNull(args);
+ switch (commandType) {
+ case COMMAND_SCROLL_TO:
+ int destX = Math.round(PixelUtil.toPixelFromDIP(args.getInt(0)));
+ int destY = Math.round(PixelUtil.toPixelFromDIP(args.getInt(1)));
+ viewManager.scrollTo(scrollView, new ScrollToCommandData(destX, destY));
+ return;
+ default:
+ throw new IllegalArgumentException(String.format(
+ "Unsupported command %d received by %s.",
+ commandType,
+ viewManager.getClass().getSimpleName()));
+ }
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewHelper.java b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewHelper.java
new file mode 100644
index 00000000000000..c0b72def6237f6
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewHelper.java
@@ -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.views.scroll;
+
+import android.os.SystemClock;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.facebook.react.bridge.ReactContext;
+import com.facebook.react.uimanager.UIManagerModule;
+
+/**
+ * Helper class that deals with emitting Scroll Events.
+ */
+public class ReactScrollViewHelper {
+
+ /**
+ * Shared by {@link ReactScrollView} and {@link ReactHorizontalScrollView}.
+ */
+ /* package */ static void emitScrollEvent(ViewGroup scrollView, int scrollX, int scrollY) {
+ View contentView = scrollView.getChildAt(0);
+ ReactContext reactContext = (ReactContext) scrollView.getContext();
+ reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher().dispatchEvent(
+ new ScrollEvent(
+ scrollView.getId(),
+ SystemClock.uptimeMillis(),
+ scrollX,
+ scrollY,
+ contentView.getWidth(),
+ contentView.getHeight(),
+ scrollView.getWidth(),
+ scrollView.getHeight()));
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewManager.java
new file mode 100644
index 00000000000000..185394151bc667
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewManager.java
@@ -0,0 +1,98 @@
+/**
+ * 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.views.scroll;
+
+import javax.annotation.Nullable;
+
+import java.util.Map;
+
+import com.facebook.react.bridge.ReadableArray;
+import com.facebook.react.common.MapBuilder;
+import com.facebook.react.uimanager.CatalystStylesDiffMap;
+import com.facebook.react.uimanager.ThemedReactContext;
+import com.facebook.react.uimanager.UIProp;
+import com.facebook.react.uimanager.ViewGroupManager;
+import com.facebook.react.views.view.ReactClippingViewGroupHelper;
+
+/**
+ * View manager for {@link ReactScrollView} components.
+ *
+ * Note that {@link ReactScrollView} and {@link ReactHorizontalScrollView} are exposed to JS
+ * as a single ScrollView component, configured via the {@code horizontal} boolean property.
+ */
+public class ReactScrollViewManager
+ extends ViewGroupManager
+ implements ReactScrollViewCommandHelper.ScrollCommandHandler {
+
+ private static final String REACT_CLASS = "RCTScrollView";
+
+ @UIProp(UIProp.Type.BOOLEAN) public static final String PROP_SHOWS_VERTICAL_SCROLL_INDICATOR =
+ "showsVerticalScrollIndicator";
+ @UIProp(UIProp.Type.BOOLEAN) public static final String PROP_SHOWS_HORIZONTAL_SCROLL_INDICATOR =
+ "showsHorizontalScrollIndicator";
+
+ @Override
+ public String getName() {
+ return REACT_CLASS;
+ }
+
+ @Override
+ public ReactScrollView createViewInstance(ThemedReactContext context) {
+ return new ReactScrollView(context);
+ }
+
+ @Override
+ public void updateView(ReactScrollView scrollView, CatalystStylesDiffMap props) {
+ super.updateView(scrollView, props);
+ if (props.hasKey(PROP_SHOWS_VERTICAL_SCROLL_INDICATOR)) {
+ scrollView.setVerticalScrollBarEnabled(
+ props.getBoolean(PROP_SHOWS_VERTICAL_SCROLL_INDICATOR, true));
+ }
+
+ if (props.hasKey(PROP_SHOWS_HORIZONTAL_SCROLL_INDICATOR)) {
+ scrollView.setHorizontalScrollBarEnabled(
+ props.getBoolean(PROP_SHOWS_HORIZONTAL_SCROLL_INDICATOR, true));
+ }
+
+ ReactClippingViewGroupHelper.applyRemoveClippedSubviewsProperty(scrollView, props);
+ }
+
+ @Override
+ public @Nullable Map getCommandsMap() {
+ return ReactScrollViewCommandHelper.getCommandsMap();
+ }
+
+ @Override
+ public void receiveCommand(
+ ReactScrollView scrollView,
+ int commandId,
+ @Nullable ReadableArray args) {
+ ReactScrollViewCommandHelper.receiveCommand(this, scrollView, commandId, args);
+ }
+
+ @Override
+ public void scrollTo(
+ ReactScrollView scrollView,
+ ReactScrollViewCommandHelper.ScrollToCommandData data) {
+ scrollView.smoothScrollTo(data.mDestX, data.mDestY);
+ }
+
+ @Override
+ public @Nullable Map getExportedCustomDirectEventTypeConstants() {
+ return MapBuilder.builder()
+ .put(ScrollEvent.EVENT_NAME, MapBuilder.of("registrationName", "onScroll"))
+ .put("topScrollBeginDrag", MapBuilder.of("registrationName", "onScrollBeginDrag"))
+ .put("topScrollEndDrag", MapBuilder.of("registrationName", "onScrollEndDrag"))
+ .put("topScrollAnimationEnd", MapBuilder.of("registrationName", "onScrollAnimationEnd"))
+ .put("topMomentumScrollBegin", MapBuilder.of("registrationName", "onMomentumScrollBegin"))
+ .put("topMomentumScrollEnd", MapBuilder.of("registrationName", "onMomentumScrollEnd"))
+ .build();
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ScrollEvent.java b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ScrollEvent.java
new file mode 100644
index 00000000000000..af49619366b744
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ScrollEvent.java
@@ -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.views.scroll;
+
+import com.facebook.react.bridge.Arguments;
+import com.facebook.react.bridge.WritableMap;
+import com.facebook.react.uimanager.PixelUtil;
+import com.facebook.react.uimanager.events.Event;
+import com.facebook.react.uimanager.events.RCTEventEmitter;
+
+/**
+ * A event dispatched from a ScrollView scrolling.
+ */
+public class ScrollEvent extends Event {
+
+ public static final String EVENT_NAME = "topScroll";
+
+ private final int mScrollX;
+ private final int mScrollY;
+ private final int mContentWidth;
+ private final int mContentHeight;
+ private final int mScrollViewWidth;
+ private final int mScrollViewHeight;
+
+ public ScrollEvent(
+ int viewTag,
+ long timestampMs,
+ int scrollX,
+ int scrollY,
+ int contentWidth,
+ int contentHeight,
+ int scrollViewWidth,
+ int scrollViewHeight) {
+ super(viewTag, timestampMs);
+ mScrollX = scrollX;
+ mScrollY = scrollY;
+ mContentWidth = contentWidth;
+ mContentHeight = contentHeight;
+ mScrollViewWidth = scrollViewWidth;
+ mScrollViewHeight = scrollViewHeight;
+ }
+
+ @Override
+ public String getEventName() {
+ return EVENT_NAME;
+ }
+
+ @Override
+ public short getCoalescingKey() {
+ // All scroll events for a given view can be coalesced
+ return 0;
+ }
+
+ @Override
+ public void dispatch(RCTEventEmitter rctEventEmitter) {
+ rctEventEmitter.receiveEvent(getViewTag(), getEventName(), serializeEventData());
+ }
+
+ private WritableMap serializeEventData() {
+ WritableMap contentOffset = Arguments.createMap();
+ contentOffset.putDouble("x", PixelUtil.toDIPFromPixel(mScrollX));
+ contentOffset.putDouble("y", PixelUtil.toDIPFromPixel(mScrollY));
+
+ WritableMap contentSize = Arguments.createMap();
+ contentSize.putDouble("width", PixelUtil.toDIPFromPixel(mContentWidth));
+ contentSize.putDouble("height", PixelUtil.toDIPFromPixel(mContentHeight));
+
+ WritableMap layoutMeasurement = Arguments.createMap();
+ layoutMeasurement.putDouble("width", PixelUtil.toDIPFromPixel(mScrollViewWidth));
+ layoutMeasurement.putDouble("height", PixelUtil.toDIPFromPixel(mScrollViewHeight));
+
+ WritableMap event = Arguments.createMap();
+ event.putMap("contentOffset", contentOffset);
+ event.putMap("contentSize", contentSize);
+ event.putMap("layoutMeasurement", layoutMeasurement);
+
+ event.putInt("target", getViewTag());
+ event.putBoolean("responderIgnoreScroll", true);
+ return event;
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/switchview/ReactSwitch.java b/ReactAndroid/src/main/java/com/facebook/react/views/switchview/ReactSwitch.java
new file mode 100644
index 00000000000000..79b39058bdefd9
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/switchview/ReactSwitch.java
@@ -0,0 +1,45 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+package com.facebook.react.views.switchviewview;
+
+import android.content.Context;
+import android.support.v7.widget.SwitchCompat;
+import android.widget.Switch;
+
+/**
+ * Switch that has its value controlled by JS. Whenever the value of the switch changes, we do not
+ * allow any other changes to that switch until JS sets a value explicitly. This stops the Switch
+ * from changing its value multiple times, when those changes have not been processed by JS first.
+ */
+/*package*/ class ReactSwitch extends SwitchCompat {
+
+ private boolean mAllowChange;
+
+ public ReactSwitch(Context context) {
+ super(context);
+ mAllowChange = true;
+ }
+
+ @Override
+ public void setChecked(boolean checked) {
+ if (mAllowChange) {
+ mAllowChange = false;
+ super.setChecked(checked);
+ }
+ }
+
+ /*package*/ void setOn(boolean on) {
+ // If the switch has a different value than the value sent by JS, we must change it.
+ if (isChecked() != on) {
+ super.setChecked(on);
+ }
+ mAllowChange = true;
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/switchview/ReactSwitchEvent.java b/ReactAndroid/src/main/java/com/facebook/react/views/switchview/ReactSwitchEvent.java
new file mode 100644
index 00000000000000..4b9251dc620627
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/switchview/ReactSwitchEvent.java
@@ -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.views.switchviewview;
+
+import com.facebook.react.bridge.Arguments;
+import com.facebook.react.bridge.WritableMap;
+import com.facebook.react.uimanager.events.Event;
+import com.facebook.react.uimanager.events.RCTEventEmitter;
+
+/**
+ * Event emitted by a ReactSwitchManager once a switch is fully switched on/off
+ */
+/*package*/ class ReactSwitchEvent extends Event {
+
+ public static final String EVENT_NAME = "topChange";
+
+ private final boolean mIsChecked;
+
+ public ReactSwitchEvent(int viewId, long timestampMs, boolean isChecked) {
+ super(viewId, timestampMs);
+ mIsChecked = isChecked;
+ }
+
+ public boolean getIsChecked() {
+ return mIsChecked;
+ }
+
+ @Override
+ public String getEventName() {
+ return EVENT_NAME;
+ }
+
+ @Override
+ public short getCoalescingKey() {
+ // All switch events for a given view can be coalesced.
+ return 0;
+ }
+
+ @Override
+ public void dispatch(RCTEventEmitter rctEventEmitter) {
+ rctEventEmitter.receiveEvent(getViewTag(), getEventName(), serializeEventData());
+ }
+
+ private WritableMap serializeEventData() {
+ WritableMap eventData = Arguments.createMap();
+ eventData.putInt("target", getViewTag());
+ eventData.putBoolean("value", getIsChecked());
+ return eventData;
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/switchview/ReactSwitchManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/switchview/ReactSwitchManager.java
new file mode 100644
index 00000000000000..9f62f5d0da117b
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/switchview/ReactSwitchManager.java
@@ -0,0 +1,119 @@
+/**
+ * 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.
+ */
+
+// switchview because switch is a keyword
+package com.facebook.react.views.switchviewview;
+
+import android.os.SystemClock;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.CompoundButton;
+
+import com.facebook.csslayout.CSSNode;
+import com.facebook.csslayout.MeasureOutput;
+import com.facebook.react.bridge.ReactContext;
+import com.facebook.react.bridge.WritableMap;
+import com.facebook.react.uimanager.CatalystStylesDiffMap;
+import com.facebook.react.uimanager.ReactShadowNode;
+import com.facebook.react.uimanager.SimpleViewManager;
+import com.facebook.react.uimanager.UIManagerModule;
+import com.facebook.react.uimanager.ThemedReactContext;
+import com.facebook.react.uimanager.UIProp;
+import com.facebook.react.uimanager.ViewProps;
+
+/**
+ * View manager for {@link ReactSwitch} components.
+ */
+public class ReactSwitchManager extends SimpleViewManager {
+
+ private static final String REACT_CLASS = "AndroidSwitch";
+ @UIProp(UIProp.Type.BOOLEAN) public static final String PROP_ENABLED = ViewProps.ENABLED;
+ @UIProp(UIProp.Type.BOOLEAN) public static final String PROP_ON = ViewProps.ON;
+
+ private static class ReactSwitchShadowNode extends ReactShadowNode implements
+ CSSNode.MeasureFunction {
+
+ private int mWidth;
+ private int mHeight;
+ private boolean mMeasured;
+
+ private ReactSwitchShadowNode() {
+ setMeasureFunction(this);
+ }
+
+ @Override
+ public void measure(CSSNode node, float width, MeasureOutput measureOutput) {
+ if (!mMeasured) {
+ // Create a switch with the default config and measure it; since we don't (currently)
+ // support setting custom switch text, this is fine, as all switches will measure the same
+ // on a specific device/theme/locale combination.
+ ReactSwitch reactSwitch = new ReactSwitch(getThemedContext());
+ final int spec = View.MeasureSpec.makeMeasureSpec(
+ ViewGroup.LayoutParams.WRAP_CONTENT,
+ View.MeasureSpec.UNSPECIFIED);
+ reactSwitch.measure(spec, spec);
+ mWidth = reactSwitch.getMeasuredWidth();
+ mHeight = reactSwitch.getMeasuredHeight();
+ mMeasured = true;
+ }
+ measureOutput.width = mWidth;
+ measureOutput.height = mHeight;
+ }
+ }
+
+ private static final CompoundButton.OnCheckedChangeListener ON_CHECKED_CHANGE_LISTENER =
+ new CompoundButton.OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ ReactContext reactContext = (ReactContext) buttonView.getContext();
+ reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher().dispatchEvent(
+ new ReactSwitchEvent(
+ buttonView.getId(),
+ SystemClock.uptimeMillis(),
+ isChecked));
+ }
+ };
+
+ @Override
+ public String getName() {
+ return REACT_CLASS;
+ }
+
+ @Override
+ public ReactShadowNode createCSSNodeInstance() {
+ return new ReactSwitchShadowNode();
+ }
+
+ @Override
+ protected ReactSwitch createViewInstance(ThemedReactContext context) {
+ ReactSwitch view = new ReactSwitch(context);
+ view.setShowText(false);
+ return view;
+ }
+
+ @Override
+ public void updateView(ReactSwitch view, CatalystStylesDiffMap props) {
+ super.updateView(view, props);
+ if (props.hasKey(PROP_ENABLED)) {
+ view.setEnabled(props.getBoolean(PROP_ENABLED, true));
+ }
+ if (props.hasKey(PROP_ON)) {
+ // we set the checked change listener to null and then restore it so that we don't fire an
+ // onChange event to JS when JS itself is updating the value of the switch
+ view.setOnCheckedChangeListener(null);
+ view.setOn(props.getBoolean(PROP_ON, false));
+ view.setOnCheckedChangeListener(ON_CHECKED_CHANGE_LISTENER);
+ }
+ }
+
+ @Override
+ protected void addEventEmitters(final ThemedReactContext reactContext, final ReactSwitch view) {
+ view.setOnCheckedChangeListener(ON_CHECKED_CHANGE_LISTENER);
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/CustomStyleSpan.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/CustomStyleSpan.java
new file mode 100644
index 00000000000000..eac5ed6db9bbab
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/CustomStyleSpan.java
@@ -0,0 +1,114 @@
+/**
+ * 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.views.text;
+
+import javax.annotation.Nullable;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import android.graphics.Paint;
+import android.graphics.Typeface;
+import android.text.TextPaint;
+import android.text.style.MetricAffectingSpan;
+
+public class CustomStyleSpan extends MetricAffectingSpan {
+
+ // Typeface caching is a bit weird: once a Typeface is created, it cannot be changed, so we need
+ // to cache each font family and each style that they have. Typeface does cache this already in
+ // Typeface.create(Typeface, style) post API 16, but for that you already need a Typeface.
+ // Therefore, here we cache one style for each font family, and let Typeface cache all styles for
+ // that font family. Of course this is not ideal, and especially after adding Typeface loading
+ // from assets, we will need to have our own caching mechanism for all Typeface creation types.
+ // TODO: t6866343 add better Typeface caching
+ private static final Map sTypefaceCache = new HashMap();
+
+ private final int mStyle;
+ private final int mWeight;
+ private final @Nullable String mFontFamily;
+
+ public CustomStyleSpan(int fontStyle, int fontWeight, @Nullable String fontFamily) {
+ mStyle = fontStyle;
+ mWeight = fontWeight;
+ mFontFamily = fontFamily;
+ }
+
+ @Override
+ public void updateDrawState(TextPaint ds) {
+ apply(ds, mStyle, mWeight, mFontFamily);
+ }
+
+ @Override
+ public void updateMeasureState(TextPaint paint) {
+ apply(paint, mStyle, mWeight, mFontFamily);
+ }
+
+ /**
+ * Returns {@link Typeface#NORMAL} or {@link Typeface#ITALIC}.
+ */
+ public int getStyle() {
+ return (mStyle == ReactTextShadowNode.UNSET ? 0 : mStyle);
+ }
+
+ /**
+ * Returns {@link Typeface#NORMAL} or {@link Typeface#BOLD}.
+ */
+ public int getWeight() {
+ return (mWeight == ReactTextShadowNode.UNSET ? 0 : mWeight);
+ }
+
+ /**
+ * Returns the font family set for this StyleSpan.
+ */
+ public @Nullable String getFontFamily() {
+ return mFontFamily;
+ }
+
+ private static void apply(Paint paint, int style, int weight, @Nullable String family) {
+ int oldStyle;
+ Typeface typeface = paint.getTypeface();
+ if (typeface == null) {
+ oldStyle = 0;
+ } else {
+ oldStyle = typeface.getStyle();
+ }
+
+ int want = 0;
+ if ((weight == Typeface.BOLD) ||
+ ((oldStyle & Typeface.BOLD) != 0 && weight == ReactTextShadowNode.UNSET)) {
+ want |= Typeface.BOLD;
+ }
+
+ if ((style == Typeface.ITALIC) ||
+ ((oldStyle & Typeface.ITALIC) != 0 && style == ReactTextShadowNode.UNSET)) {
+ want |= Typeface.ITALIC;
+ }
+
+ if (family != null) {
+ typeface = getOrCreateTypeface(family, want);
+ }
+
+ if (typeface != null) {
+ paint.setTypeface(Typeface.create(typeface, want));
+ } else {
+ paint.setTypeface(Typeface.defaultFromStyle(want));
+ }
+ }
+
+ private static Typeface getOrCreateTypeface(String family, int style) {
+ if (sTypefaceCache.get(family) != null) {
+ return sTypefaceCache.get(family);
+ }
+
+ Typeface typeface = Typeface.create(family, style);
+ sTypefaceCache.put(family, typeface);
+ return typeface;
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/DefaultStyleValuesUtil.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/DefaultStyleValuesUtil.java
new file mode 100644
index 00000000000000..78aa65a3d1e93d
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/DefaultStyleValuesUtil.java
@@ -0,0 +1,65 @@
+/**
+ * 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.views.text;
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+
+/**
+ * Utility class that access default values from style
+ */
+public final class DefaultStyleValuesUtil {
+
+ private DefaultStyleValuesUtil() {
+ throw new AssertionError("Never invoke this for an Utility class!");
+ }
+
+ /**
+ * Utility method that returns the default text hint color as define by the theme
+ *
+ * @param context The Context
+ * @return The ColorStateList for the hint text as defined in the style
+ */
+ public static ColorStateList getDefaultTextColorHint(Context context) {
+ Resources.Theme theme = context.getTheme();
+ TypedArray textAppearances = null;
+ try {
+ textAppearances = theme.obtainStyledAttributes(new int[]{android.R.attr.textColorHint});
+ ColorStateList textColorHint = textAppearances.getColorStateList(0);
+ return textColorHint;
+ } finally {
+ if (textAppearances != null) {
+ textAppearances.recycle();
+ }
+ }
+ }
+
+ /**
+ * Utility method that returns the default text color as define by the theme
+ *
+ * @param context The Context
+ * @return The ColorStateList for the text as defined in the style
+ */
+ public static ColorStateList getDefaultTextColor(Context context) {
+ Resources.Theme theme = context.getTheme();
+ TypedArray textAppearances = null;
+ try {
+ textAppearances = theme.obtainStyledAttributes(new int[]{android.R.attr.textColor});
+ ColorStateList textColor = textAppearances.getColorStateList(0);
+ return textColor;
+ } finally {
+ if (textAppearances != null) {
+ textAppearances.recycle();
+ }
+ }
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactRawTextManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactRawTextManager.java
new file mode 100644
index 00000000000000..1dec2e7265e10b
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactRawTextManager.java
@@ -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.views.text;
+
+import com.facebook.react.uimanager.CatalystStylesDiffMap;
+import com.facebook.react.common.annotations.VisibleForTesting;
+import com.facebook.react.uimanager.ThemedReactContext;
+
+/**
+ * Manages raw text nodes. Since they are used only as a virtual nodes any type of native view
+ * operation will throw an {@link IllegalStateException}
+ */
+public class ReactRawTextManager extends ReactTextViewManager {
+
+ @VisibleForTesting
+ public static final String REACT_CLASS = "RCTRawText";
+
+ @Override
+ public String getName() {
+ return REACT_CLASS;
+ }
+
+ @Override
+ public ReactTextView createViewInstance(ThemedReactContext context) {
+ throw new IllegalStateException("RKRawText doesn't map into a native view");
+ }
+
+ @Override
+ public void updateView(ReactTextView view, CatalystStylesDiffMap props) {
+ throw new IllegalStateException("RKRawText doesn't map into a native view");
+ }
+
+ @Override
+ public void updateExtraData(ReactTextView view, Object extraData) {
+ }
+
+ @Override
+ public ReactTextShadowNode createCSSNodeInstance() {
+ return new ReactTextShadowNode(true);
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTagSpan.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTagSpan.java
new file mode 100644
index 00000000000000..9bdc7c03f61941
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTagSpan.java
@@ -0,0 +1,27 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+package com.facebook.react.views.text;
+
+/**
+ * Instances of this class are used to place reactTag information of nested text react nodes
+ * into spannable text rendered by single {@link TextView}
+ */
+public class ReactTagSpan {
+
+ private final int mReactTag;
+
+ public ReactTagSpan(int reactTag) {
+ mReactTag = reactTag;
+ }
+
+ public int getReactTag() {
+ return mReactTag;
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextShadowNode.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextShadowNode.java
new file mode 100644
index 00000000000000..dcd997b6c10ecc
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextShadowNode.java
@@ -0,0 +1,394 @@
+/**
+ * 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.views.text;
+
+import javax.annotation.Nullable;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import android.graphics.Typeface;
+import android.text.BoringLayout;
+import android.text.Layout;
+import android.text.Spannable;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.StaticLayout;
+import android.text.TextPaint;
+import android.text.style.AbsoluteSizeSpan;
+import android.text.style.BackgroundColorSpan;
+import android.text.style.ForegroundColorSpan;
+import android.widget.TextView;
+
+import com.facebook.csslayout.CSSConstants;
+import com.facebook.csslayout.CSSNode;
+import com.facebook.csslayout.MeasureOutput;
+import com.facebook.infer.annotation.Assertions;
+import com.facebook.react.uimanager.CSSColorUtil;
+import com.facebook.react.uimanager.CatalystStylesDiffMap;
+import com.facebook.react.uimanager.IllegalViewOperationException;
+import com.facebook.react.uimanager.PixelUtil;
+import com.facebook.react.uimanager.ReactShadowNode;
+import com.facebook.react.uimanager.UIViewOperationQueue;
+import com.facebook.react.uimanager.ViewDefaults;
+import com.facebook.react.uimanager.ViewProps;
+
+/**
+ * {@link ReactShadowNode} class for spannable text view.
+ *
+ * This node calculates {@link Spannable} based on subnodes of the same type and passes the
+ * resulting object down to textview's shadowview and actual native {@link TextView} instance.
+ * It is important to keep in mind that {@link Spannable} is calculated only on layout step, so if
+ * there are any text properties that may/should affect the result of {@link Spannable} they should
+ * be set in a corresponding {@link ReactTextShadowNode}. Resulting {@link Spannable} object is then
+ * then passed as "computedDataFromMeasure" down to shadow and native view.
+ *
+ * TODO(7255858): Rename *CSSNode to *ShadowView (or sth similar) as it's no longer is used
+ * solely for layouting
+ */
+public class ReactTextShadowNode extends ReactShadowNode {
+
+ public static final String PROP_TEXT = "text";
+ public static final int UNSET = -1;
+
+ private static final TextPaint sTextPaintInstance = new TextPaint();
+
+ static {
+ sTextPaintInstance.setFlags(TextPaint.ANTI_ALIAS_FLAG);
+ }
+
+ private static class SetSpanOperation {
+ protected int start, end;
+ protected Object what;
+ SetSpanOperation(int start, int end, Object what) {
+ this.start = start;
+ this.end = end;
+ this.what = what;
+ }
+ public void execute(SpannableStringBuilder sb) {
+ sb.setSpan(what, start, end, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
+ }
+ }
+
+ private static final void buildSpannedFromTextCSSNode(
+ ReactTextShadowNode textCSSNode,
+ SpannableStringBuilder sb,
+ List ops) {
+ int start = sb.length();
+ if (textCSSNode.mText != null) {
+ sb.append(textCSSNode.mText);
+ }
+ for (int i = 0, length = textCSSNode.getChildCount(); i < length; i++) {
+ CSSNode child = textCSSNode.getChildAt(i);
+ if (child instanceof ReactTextShadowNode) {
+ buildSpannedFromTextCSSNode((ReactTextShadowNode) child, sb, ops);
+ } else {
+ throw new IllegalViewOperationException("Unexpected view type nested under text node: "
+ + child.getClass());
+ }
+ ((ReactTextShadowNode) child).markUpdateSeen();
+ }
+ int end = sb.length();
+ if (end > start) {
+ if (textCSSNode.mIsColorSet) {
+ ops.add(new SetSpanOperation(start, end, new ForegroundColorSpan(textCSSNode.mColor)));
+ }
+ if (textCSSNode.mIsBackgroundColorSet) {
+ ops.add(
+ new SetSpanOperation(
+ start,
+ end,
+ new BackgroundColorSpan(textCSSNode.mBackgroundColor)));
+ }
+ if (textCSSNode.mFontSize != UNSET) {
+ ops.add(new SetSpanOperation(start, end, new AbsoluteSizeSpan(textCSSNode.mFontSize)));
+ }
+ if (textCSSNode.mFontStyle != UNSET ||
+ textCSSNode.mFontWeight != UNSET ||
+ textCSSNode.mFontFamily != null) {
+ ops.add(new SetSpanOperation(
+ start,
+ end,
+ new CustomStyleSpan(
+ textCSSNode.mFontStyle,
+ textCSSNode.mFontWeight,
+ textCSSNode.mFontFamily)));
+ }
+ ops.add(new SetSpanOperation(start, end, new ReactTagSpan(textCSSNode.getReactTag())));
+ }
+ }
+
+ protected static final Spanned fromTextCSSNode(ReactTextShadowNode textCSSNode) {
+ SpannableStringBuilder sb = new SpannableStringBuilder();
+ // TODO(5837930): Investigate whether it's worth optimizing this part and do it if so
+
+ // The {@link SpannableStringBuilder} implementation require setSpan operation to be called
+ // up-to-bottom, otherwise all the spannables that are withing the region for which one may set
+ // a new spannable will be wiped out
+ List ops = new ArrayList();
+ buildSpannedFromTextCSSNode(textCSSNode, sb, ops);
+ if (textCSSNode.mFontSize == -1) {
+ sb.setSpan(
+ new AbsoluteSizeSpan((int) Math.ceil(PixelUtil.toPixelFromSP(ViewDefaults.FONT_SIZE_SP))),
+ 0,
+ sb.length(),
+ Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
+ }
+ for (int i = ops.size() - 1; i >= 0; i--) {
+ SetSpanOperation op = ops.get(i);
+ op.execute(sb);
+ }
+ return sb;
+ }
+
+ private static final CSSNode.MeasureFunction TEXT_MEASURE_FUNCTION =
+ new CSSNode.MeasureFunction() {
+ @Override
+ public void measure(CSSNode node, float width, MeasureOutput measureOutput) {
+ // TODO(5578671): Handle text direction (see View#getTextDirectionHeuristic)
+ ReactTextShadowNode reactCSSNode = (ReactTextShadowNode) node;
+ TextPaint textPaint = sTextPaintInstance;
+ Layout layout;
+ Spanned text = Assertions.assertNotNull(
+ reactCSSNode.mPreparedSpannedText,
+ "Spannable element has not been prepared in onBeforeLayout");
+ BoringLayout.Metrics boring = BoringLayout.isBoring(text, textPaint);
+ float desiredWidth = boring == null ?
+ Layout.getDesiredWidth(text, textPaint) : Float.NaN;
+
+ if (boring == null &&
+ (CSSConstants.isUndefined(width) ||
+ (!CSSConstants.isUndefined(desiredWidth) && desiredWidth <= width))) {
+ // Is used when the width is not known and the text is not boring, ie. if it contains
+ // unicode characters.
+ layout = new StaticLayout(
+ text,
+ textPaint,
+ (int) Math.ceil(desiredWidth),
+ Layout.Alignment.ALIGN_NORMAL,
+ 1,
+ 0,
+ true);
+ } else if (boring != null && (CSSConstants.isUndefined(width) || boring.width <= width)) {
+ // Is used for single-line, boring text when the width is either unknown or bigger
+ // than the width of the text.
+ layout = BoringLayout.make(
+ text,
+ textPaint,
+ boring.width,
+ Layout.Alignment.ALIGN_NORMAL,
+ 1,
+ 0,
+ boring,
+ true);
+ } else {
+ // Is used for multiline, boring text and the width is known.
+ layout = new StaticLayout(
+ text,
+ textPaint,
+ (int) width,
+ Layout.Alignment.ALIGN_NORMAL,
+ 1,
+ 0,
+ true);
+ }
+
+ measureOutput.height = layout.getHeight();
+ measureOutput.width = layout.getWidth();
+ if (reactCSSNode.mNumberOfLines != UNSET &&
+ reactCSSNode.mNumberOfLines < layout.getLineCount()) {
+ measureOutput.height = layout.getLineBottom(reactCSSNode.mNumberOfLines - 1);
+ }
+ if (reactCSSNode.mLineHeight != UNSET) {
+ int lines = reactCSSNode.mNumberOfLines != UNSET
+ ? Math.min(reactCSSNode.mNumberOfLines, layout.getLineCount())
+ : layout.getLineCount();
+ float lineHeight = PixelUtil.toPixelFromSP(reactCSSNode.mLineHeight);
+ measureOutput.height = lineHeight * lines;
+ }
+ }
+ };
+
+ /**
+ * Return -1 if the input string is not a valid numeric fontWeight (100, 200, ..., 900), otherwise
+ * return the weight.
+ */
+ private static int parseNumericFontWeight(String fontWeightString) {
+ // This should be much faster than using regex to verify input and Integer.parseInt
+ return fontWeightString.length() == 3 && fontWeightString.endsWith("00")
+ && fontWeightString.charAt(0) <= '9' && fontWeightString.charAt(0) >= '1' ?
+ 100 * (fontWeightString.charAt(0) - '0') : -1;
+ }
+
+ private int mLineHeight = UNSET;
+ private int mNumberOfLines = UNSET;
+ private boolean mIsColorSet = false;
+ private int mColor;
+ private boolean mIsBackgroundColorSet = false;
+ private int mBackgroundColor;
+ private int mFontSize = UNSET;
+ /**
+ * mFontStyle can be {@link Typeface#NORMAL} or {@link Typeface#ITALIC}.
+ * mFontWeight can be {@link Typeface#NORMAL} or {@link Typeface#BOLD}.
+ */
+ private int mFontStyle = UNSET;
+ private int mFontWeight = UNSET;
+ /**
+ * NB: If a font family is used that does not have a style in a certain Android version (ie.
+ * monospace bold pre Android 5.0), that style (ie. bold) will not be inherited by nested Text
+ * nodes. To retain that style, you have to add it to those nodes explicitly.
+ * Example, Android 4.4:
+ * Bold Text
+ * Bold Text
+ * Bold Text
+ *
+ * Not Bold Text
+ * Not Bold Text
+ * Not Bold Text
+ *
+ * Not Bold Text
+ * Bold Text
+ * Bold Text
+ */
+ private @Nullable String mFontFamily = null;
+ private @Nullable String mText = null;
+
+ private @Nullable Spanned mPreparedSpannedText;
+ private final boolean mIsVirtual;
+
+ @Override
+ public void onBeforeLayout() {
+ if (mIsVirtual) {
+ return;
+ }
+ mPreparedSpannedText = fromTextCSSNode(this);
+ markUpdated();
+ }
+
+ @Override
+ protected void markUpdated() {
+ super.markUpdated();
+ // We mark virtual anchor node as dirty as updated text needs to be re-measured
+ if (!mIsVirtual) {
+ super.dirty();
+ }
+ }
+
+ @Override
+ public void updateProperties(CatalystStylesDiffMap styles) {
+ super.updateProperties(styles);
+
+ if (styles.hasKey(PROP_TEXT)) {
+ mText = styles.getString(PROP_TEXT);
+ markUpdated();
+ }
+ if (styles.hasKey(ViewProps.NUMBER_OF_LINES)) {
+ mNumberOfLines = styles.getInt(ViewProps.NUMBER_OF_LINES, UNSET);
+ markUpdated();
+ }
+ if (styles.hasKey(ViewProps.LINE_HEIGHT)) {
+ mLineHeight = styles.getInt(ViewProps.LINE_HEIGHT, UNSET);
+ markUpdated();
+ }
+ if (styles.hasKey(ViewProps.FONT_SIZE)) {
+ if (styles.isNull(ViewProps.FONT_SIZE)) {
+ mFontSize = UNSET;
+ } else {
+ mFontSize = (int) Math.ceil(PixelUtil.toPixelFromSP(
+ styles.getFloat(ViewProps.FONT_SIZE, ViewDefaults.FONT_SIZE_SP)));
+ }
+ markUpdated();
+ }
+ if (styles.hasKey(ViewProps.COLOR)) {
+ String colorString = styles.getString(ViewProps.COLOR);
+ if (colorString == null) {
+ mIsColorSet = false;
+ } else {
+ mColor = CSSColorUtil.getColor(colorString);
+ mIsColorSet = true;
+ }
+ markUpdated();
+ }
+ if (styles.hasKey(ViewProps.BACKGROUND_COLOR)) {
+ String colorString = styles.getString(ViewProps.BACKGROUND_COLOR);
+ if (colorString == null) {
+ mIsBackgroundColorSet = false;
+ } else {
+ mBackgroundColor = CSSColorUtil.getColor(colorString);
+ mIsBackgroundColorSet = true;
+ }
+ markUpdated();
+ }
+
+ if (styles.hasKey(ViewProps.FONT_FAMILY)) {
+ mFontFamily = styles.getString(ViewProps.FONT_FAMILY);
+ markUpdated();
+ }
+
+ if (styles.hasKey(ViewProps.FONT_WEIGHT)) {
+ String fontWeightString = styles.getString(ViewProps.FONT_WEIGHT);
+ int fontWeightNumeric = fontWeightString != null ?
+ parseNumericFontWeight(fontWeightString) : -1;
+ int fontWeight = UNSET;
+ if (fontWeightNumeric >= 500 || "bold".equals(fontWeightString)) {
+ fontWeight = Typeface.BOLD;
+ } else if ("normal".equals(fontWeightString) ||
+ (fontWeightNumeric != -1 && fontWeightNumeric < 500)) {
+ fontWeight = Typeface.NORMAL;
+ }
+ if (fontWeight != mFontWeight) {
+ mFontWeight = fontWeight;
+ markUpdated();
+ }
+ }
+
+ if (styles.hasKey(ViewProps.FONT_STYLE)) {
+ String fontStyleString = styles.getString(ViewProps.FONT_STYLE);
+ int fontStyle = UNSET;
+ if ("italic".equals(fontStyleString)) {
+ fontStyle = Typeface.ITALIC;
+ } else if ("normal".equals(fontStyleString)) {
+ fontStyle = Typeface.NORMAL;
+ }
+ if (fontStyle != mFontStyle) {
+ mFontStyle = fontStyle;
+ markUpdated();
+ }
+ }
+ }
+
+ @Override
+ public boolean isVirtualAnchor() {
+ return !mIsVirtual;
+ }
+
+ @Override
+ public boolean isVirtual() {
+ return mIsVirtual;
+ }
+
+ @Override
+ public void onCollectExtraUpdates(UIViewOperationQueue uiViewOperationQueue) {
+ if (mIsVirtual) {
+ return;
+ }
+ super.onCollectExtraUpdates(uiViewOperationQueue);
+ if (mPreparedSpannedText != null) {
+ uiViewOperationQueue.enqueueUpdateExtraData(getReactTag(), mPreparedSpannedText);
+ }
+ }
+
+ public ReactTextShadowNode(boolean isVirtual) {
+ mIsVirtual = isVirtual;
+ if (!isVirtual) {
+ setMeasureFunction(TEXT_MEASURE_FUNCTION);
+ }
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java
new file mode 100644
index 00000000000000..36d3923fec8218
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java
@@ -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.views.text;
+
+import android.content.Context;
+import android.text.Layout;
+import android.text.Spanned;
+import android.widget.TextView;
+
+import com.facebook.react.uimanager.ReactCompoundView;
+
+public class ReactTextView extends TextView implements ReactCompoundView {
+
+ public ReactTextView(Context context) {
+ super(context);
+ }
+
+ @Override
+ public int reactTagForTouch(float touchX, float touchY) {
+ Spanned text = (Spanned) getText();
+ int target = getId();
+
+ int x = (int) touchX;
+ int y = (int) touchY;
+
+ x -= getTotalPaddingLeft();
+ y -= getTotalPaddingTop();
+
+ x += getScrollX();
+ y += getScrollY();
+
+ Layout layout = getLayout();
+ int line = layout.getLineForVertical(y);
+
+ int lineStartX = (int) layout.getLineLeft(line);
+ int lineEndX = (int) layout.getLineRight(line);
+
+ // TODO(5966918): Consider extending touchable area for text spans by some DP constant
+ if (x >= lineStartX && x <= lineEndX) {
+ int index = layout.getOffsetForHorizontal(line, x);
+
+ // We choose the most inner span (shortest) containing character at the given index
+ // if no such span can be found we will send the textview's react id as a touch handler
+ // In case when there are more than one spans with same length we choose the last one
+ // from the spans[] array, since it correspond to the most inner react element
+ ReactTagSpan[] spans = text.getSpans(index, index, ReactTagSpan.class);
+
+ if (spans != null) {
+ int targetSpanTextLength = text.length();
+ for (int i = 0; i < spans.length; i++) {
+ int spanStart = text.getSpanStart(spans[i]);
+ int spanEnd = text.getSpanEnd(spans[i]);
+ if (spanEnd > index && (spanEnd - spanStart) <= targetSpanTextLength) {
+ target = spans[i].getReactTag();
+ targetSpanTextLength = (spanEnd - spanStart);
+ }
+ }
+ }
+ }
+
+ return target;
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.java
new file mode 100644
index 00000000000000..e78b15cefd91d9
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.java
@@ -0,0 +1,103 @@
+/**
+ * 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.views.text;
+
+import android.text.Spannable;
+import android.text.Spanned;
+import android.text.TextUtils;
+import android.view.Gravity;
+import android.widget.TextView;
+
+import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
+import com.facebook.react.uimanager.BaseViewPropertyApplicator;
+import com.facebook.react.uimanager.CatalystStylesDiffMap;
+import com.facebook.react.uimanager.PixelUtil;
+import com.facebook.react.uimanager.ThemedReactContext;
+import com.facebook.react.uimanager.UIProp;
+import com.facebook.react.uimanager.ViewDefaults;
+import com.facebook.react.uimanager.ViewManager;
+import com.facebook.react.uimanager.ViewProps;
+import com.facebook.react.common.annotations.VisibleForTesting;
+
+/**
+ * Manages instances of spannable {@link TextView}.
+ *
+ * This is a "shadowing" view manager, which means that the {@link NativeViewHierarchyManager} will
+ * not manage children of native {@link TextView} instances returned by this manager. Instead we use
+ * @{link ReactTextShadowNode} hierarchy to calculate a {@link Spannable} text representing the
+ * whole text subtree.
+ */
+public class ReactTextViewManager extends ViewManager {
+
+ @VisibleForTesting
+ public static final String REACT_CLASS = "RCTText";
+
+ @UIProp(UIProp.Type.NUMBER)
+ public static final String PROP_NUMBER_OF_LINES = ViewProps.NUMBER_OF_LINES;
+ @UIProp(UIProp.Type.STRING)
+ public static final String PROP_TEXT_ALIGN = ViewProps.TEXT_ALIGN;
+ @UIProp(UIProp.Type.NUMBER)
+ public static final String PROP_LINE_HEIGHT = ViewProps.LINE_HEIGHT;
+
+ @Override
+ public String getName() {
+ return REACT_CLASS;
+ }
+
+ @Override
+ public ReactTextView createViewInstance(ThemedReactContext context) {
+ return new ReactTextView(context);
+ }
+
+ @Override
+ public void updateView(ReactTextView view, CatalystStylesDiffMap props) {
+ BaseViewPropertyApplicator.applyCommonViewProperties(view, props);
+ // maxLines can only be set in master view (block), doesn't really make sense to set in a span
+ if (props.hasKey(PROP_NUMBER_OF_LINES)) {
+ view.setMaxLines(props.getInt(PROP_NUMBER_OF_LINES, ViewDefaults.NUMBER_OF_LINES));
+ view.setEllipsize(TextUtils.TruncateAt.END);
+ }
+ // same with textAlign
+ if (props.hasKey(PROP_TEXT_ALIGN)) {
+ final String textAlign = props.getString(PROP_TEXT_ALIGN);
+ if (textAlign == null || "auto".equals(textAlign)) {
+ view.setGravity(Gravity.NO_GRAVITY);
+ } else if ("left".equals(textAlign)) {
+ view.setGravity(Gravity.LEFT);
+ } else if ("right".equals(textAlign)) {
+ view.setGravity(Gravity.RIGHT);
+ } else if ("center".equals(textAlign)) {
+ view.setGravity(Gravity.CENTER_HORIZONTAL);
+ } else {
+ throw new JSApplicationIllegalArgumentException("Invalid textAlign: " + textAlign);
+ }
+ }
+ // same for lineSpacing
+ if (props.hasKey(PROP_LINE_HEIGHT)) {
+ if (props.isNull(PROP_LINE_HEIGHT)) {
+ view.setLineSpacing(0, 1);
+ } else {
+ float lineHeight =
+ PixelUtil.toPixelFromSP(props.getInt(PROP_LINE_HEIGHT, ViewDefaults.LINE_HEIGHT));
+ view.setLineSpacing(lineHeight, 0);
+ }
+ }
+ }
+
+ @Override
+ public void updateExtraData(ReactTextView view, Object extraData) {
+ view.setText((Spanned) extraData);
+ }
+
+ @Override
+ public ReactTextShadowNode createCSSNodeInstance() {
+ return new ReactTextShadowNode(false);
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactVirtualTextViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactVirtualTextViewManager.java
new file mode 100644
index 00000000000000..b11af8c5725da6
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactVirtualTextViewManager.java
@@ -0,0 +1,27 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+package com.facebook.react.views.text;
+
+import com.facebook.react.common.annotations.VisibleForTesting;
+
+/**
+ * Manages raw text nodes. Since they are used only as a virtual nodes any type of native view
+ * operation will throw an {@link IllegalStateException}
+ */
+public class ReactVirtualTextViewManager extends ReactRawTextManager {
+
+ @VisibleForTesting
+ public static final String REACT_CLASS = "RCTVirtualText";
+
+ @Override
+ public String getName() {
+ return REACT_CLASS;
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java
new file mode 100644
index 00000000000000..863d996079bd92
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java
@@ -0,0 +1,275 @@
+/**
+ * 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.views.textinput;
+
+import javax.annotation.Nullable;
+
+import java.util.ArrayList;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.text.Editable;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.TextWatcher;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.EditText;
+
+import com.facebook.infer.annotation.Assertions;
+
+/**
+ * A wrapper around the EditText that lets us better control what happens when an EditText gets
+ * focused or blurred, and when to display the soft keyboard and when not to.
+ *
+ * ReactEditTexts have setFocusableInTouchMode set to false automatically because touches on the
+ * EditText are managed on the JS side. This also removes the nasty side effect that EditTexts
+ * have, which is that focus is always maintained on one of the EditTexts.
+ *
+ * The wrapper stops the EditText from triggering *TextChanged events, in the case where JS
+ * has called this explicitly. This is the default behavior on other platforms as well.
+ * VisibleForTesting from {@link TextInputEventsTestCase}.
+ */
+public class ReactEditText extends EditText {
+
+ private final InputMethodManager mInputMethodManager;
+ // This flag is set to true when we set the text of the EditText explicitly. In that case, no
+ // *TextChanged events should be triggered. This is less expensive than removing the text
+ // listeners and adding them back again after the text change is completed.
+ private boolean mIsSettingTextFromJS;
+ // This component is controlled, so we want it to get focused only when JS ask it to do so.
+ // Whenever android requests focus (which it does for random reasons), it will be ignored.
+ private boolean mIsJSSettingFocus;
+ private int mDefaultGravityHorizontal;
+ private int mDefaultGravityVertical;
+ private int mNativeEventCount;
+ private @Nullable ArrayList mListeners;
+ private @Nullable TextWatcherDelegator mTextWatcherDelegator;
+
+ public ReactEditText(Context context) {
+ super(context);
+ setFocusableInTouchMode(false);
+
+ mInputMethodManager = (InputMethodManager)
+ Assertions.assertNotNull(getContext().getSystemService(Context.INPUT_METHOD_SERVICE));
+ mDefaultGravityHorizontal =
+ getGravity() & (Gravity.HORIZONTAL_GRAVITY_MASK | Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK);
+ mDefaultGravityVertical = getGravity() & Gravity.VERTICAL_GRAVITY_MASK;
+ mNativeEventCount = 0;
+ mIsSettingTextFromJS = false;
+ mIsJSSettingFocus = false;
+ mListeners = null;
+ mTextWatcherDelegator = null;
+ }
+
+ // After the text changes inside an EditText, TextView checks if a layout() has been requested.
+ // If it has, it will not scroll the text to the end of the new text inserted, but wait for the
+ // next layout() to be called. However, we do not perform a layout() after a requestLayout(), so
+ // we need to override isLayoutRequested to force EditText to scroll to the end of the new text
+ // immediately.
+ // TODO: t6408636 verify if we should schedule a layout after a View does a requestLayout()
+ @Override
+ public boolean isLayoutRequested() {
+ return false;
+ }
+
+ // Consume 'Enter' key events: TextView tries to give focus to the next TextInput, but it can't
+ // since we only allow JS to change focus, which in turn causes TextView to crash.
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_ENTER) {
+ hideSoftKeyboard();
+ return true;
+ }
+ return super.onKeyUp(keyCode, event);
+ }
+
+ @Override
+ public void clearFocus() {
+ setFocusableInTouchMode(false);
+ super.clearFocus();
+ hideSoftKeyboard();
+ }
+
+ @Override
+ public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
+ if (!mIsJSSettingFocus) {
+ return false;
+ }
+ setFocusableInTouchMode(true);
+ boolean focused = super.requestFocus(direction, previouslyFocusedRect);
+ showSoftKeyboard();
+ return focused;
+ }
+
+ @Override
+ public void addTextChangedListener(TextWatcher watcher) {
+ if (mListeners == null) {
+ mListeners = new ArrayList<>();
+ super.addTextChangedListener(getTextWatcherDelegator());
+ }
+
+ mListeners.add(watcher);
+ }
+
+ @Override
+ public void removeTextChangedListener(TextWatcher watcher) {
+ if (mListeners != null) {
+ mListeners.remove(watcher);
+
+ if (mListeners.isEmpty()) {
+ mListeners = null;
+ super.removeTextChangedListener(getTextWatcherDelegator());
+ }
+ }
+ }
+
+ /* package */ void requestFocusFromJS() {
+ mIsJSSettingFocus = true;
+ requestFocus();
+ mIsJSSettingFocus = false;
+ }
+
+ /* package */ void clearFocusFromJS() {
+ clearFocus();
+ }
+
+ // VisibleForTesting from {@link TextInputEventsTestCase}.
+ public int incrementAndGetEventCounter() {
+ return ++mNativeEventCount;
+ }
+
+ // VisibleForTesting from {@link TextInputEventsTestCase}.
+ public void maybeSetText(ReactTextUpdate reactTextUpdate) {
+ // Only set the text if it is up to date.
+ if (reactTextUpdate.getJsEventCounter() < mNativeEventCount) {
+ return;
+ }
+
+ // The current text gets replaced with the text received from JS. However, the spans on the
+ // current text need to be adapted to the new text. Since TextView#setText() will remove or
+ // reset some of these spans even if they are set directly, SpannableStringBuilder#replace() is
+ // used instead (this is also used by the the keyboard implementation underneath the covers).
+ SpannableStringBuilder spannableStringBuilder =
+ new SpannableStringBuilder(reactTextUpdate.getText());
+ manageSpans(spannableStringBuilder);
+ mIsSettingTextFromJS = true;
+ getText().replace(0, length(), spannableStringBuilder);
+ mIsSettingTextFromJS = false;
+ }
+
+ /**
+ * Remove and/or add {@link Spanned.SPAN_EXCLUSIVE_EXCLUSIVE} spans, since they should only exist
+ * as long as the text they cover is the same. All other spans will remain the same, since they
+ * will adapt to the new text, hence why {@link SpannableStringBuilder#replace} never removes
+ * them.
+ */
+ private void manageSpans(SpannableStringBuilder spannableStringBuilder) {
+ Object[] spans = getText().getSpans(0, length(), Object.class);
+ for (int spanIdx = 0; spanIdx < spans.length; spanIdx++) {
+ if ((getText().getSpanFlags(spans[spanIdx]) & Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) !=
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) {
+ continue;
+ }
+ Object span = spans[spanIdx];
+ final int spanStart = getText().getSpanStart(spans[spanIdx]);
+ final int spanEnd = getText().getSpanEnd(spans[spanIdx]);
+ final int spanFlags = getText().getSpanFlags(spans[spanIdx]);
+
+ // Make sure the span is removed from existing text, otherwise the spans we set will be
+ // ignored or it will cover text that has changed.
+ getText().removeSpan(spans[spanIdx]);
+ if (sameTextForSpan(getText(), spannableStringBuilder, spanStart, spanEnd)) {
+ spannableStringBuilder.setSpan(span, spanStart, spanEnd, spanFlags);
+ }
+ }
+ }
+
+ private static boolean sameTextForSpan(
+ final Editable oldText,
+ final SpannableStringBuilder newText,
+ final int start,
+ final int end) {
+ if (start > newText.length() || end > newText.length()) {
+ return false;
+ }
+ for (int charIdx = start; charIdx < end; charIdx++) {
+ if (oldText.charAt(charIdx) != newText.charAt(charIdx)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private boolean showSoftKeyboard() {
+ return mInputMethodManager.showSoftInput(this, 0);
+ }
+
+ private void hideSoftKeyboard() {
+ mInputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0);
+ }
+
+ private TextWatcherDelegator getTextWatcherDelegator() {
+ if (mTextWatcherDelegator == null) {
+ mTextWatcherDelegator = new TextWatcherDelegator();
+ }
+ return mTextWatcherDelegator;
+ }
+
+ /* package */ void setGravityHorizontal(int gravityHorizontal) {
+ if (gravityHorizontal == 0) {
+ gravityHorizontal = mDefaultGravityHorizontal;
+ }
+ setGravity(
+ (getGravity() & ~Gravity.HORIZONTAL_GRAVITY_MASK &
+ ~Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) | gravityHorizontal);
+ }
+
+ /* package */ void setGravityVertical(int gravityVertical) {
+ if (gravityVertical == 0) {
+ gravityVertical = mDefaultGravityVertical;
+ }
+ setGravity((getGravity() & ~Gravity.VERTICAL_GRAVITY_MASK) | gravityVertical);
+ }
+
+ /**
+ * This class will redirect *TextChanged calls to the listeners only in the case where the text
+ * is changed by the user, and not explicitly set by JS.
+ */
+ private class TextWatcherDelegator implements TextWatcher {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ if (!mIsSettingTextFromJS && mListeners != null) {
+ for (TextWatcher listener : mListeners) {
+ listener.beforeTextChanged(s, start, count, after);
+ }
+ }
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ if (!mIsSettingTextFromJS && mListeners != null) {
+ for (TextWatcher listener : mListeners) {
+ listener.onTextChanged(s, start, before, count);
+ }
+ }
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ if (!mIsSettingTextFromJS && mListeners != null) {
+ for (android.text.TextWatcher listener : mListeners) {
+ listener.afterTextChanged(s);
+ }
+ }
+ }
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextChangedEvent.java b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextChangedEvent.java
new file mode 100644
index 00000000000000..f7363441b92aef
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextChangedEvent.java
@@ -0,0 +1,66 @@
+/**
+ * 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.views.textinput;
+
+import com.facebook.react.bridge.Arguments;
+import com.facebook.react.bridge.WritableMap;
+import com.facebook.react.uimanager.events.Event;
+import com.facebook.react.uimanager.events.RCTEventEmitter;
+
+/**
+ * Event emitted by EditText native view when text changes.
+ */
+/* package */ class ReactTextChangedEvent extends Event {
+
+ public static final String EVENT_NAME = "topChange";
+
+ private String mText;
+ private int mContentWidth;
+ private int mContentHeight;
+ private int mEventCount;
+
+ public ReactTextChangedEvent(
+ int viewId,
+ long timestampMs,
+ String text,
+ int contentSizeWidth,
+ int contentSizeHeight,
+ int eventCount) {
+ super(viewId, timestampMs);
+ mText = text;
+ mContentWidth = contentSizeWidth;
+ mContentHeight = contentSizeHeight;
+ mEventCount = eventCount;
+ }
+
+ @Override
+ public String getEventName() {
+ return EVENT_NAME;
+ }
+
+ @Override
+ public void dispatch(RCTEventEmitter rctEventEmitter) {
+ rctEventEmitter.receiveEvent(getViewTag(), getEventName(), serializeEventData());
+ }
+
+ private WritableMap serializeEventData() {
+ WritableMap eventData = Arguments.createMap();
+ eventData.putString("text", mText);
+
+ WritableMap contentSize = Arguments.createMap();
+ contentSize.putDouble("width", mContentWidth);
+ contentSize.putDouble("height", mContentHeight);
+ eventData.putMap("contentSize", contentSize);
+ eventData.putInt("eventCount", mEventCount);
+
+ eventData.putInt("target", getViewTag());
+ return eventData;
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputBlurEvent.java b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputBlurEvent.java
new file mode 100644
index 00000000000000..2b77c314083bf6
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputBlurEvent.java
@@ -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.views.textinput;
+
+import com.facebook.react.bridge.Arguments;
+import com.facebook.react.bridge.WritableMap;
+import com.facebook.react.uimanager.events.Event;
+import com.facebook.react.uimanager.events.RCTEventEmitter;
+
+/**
+ * Event emitted by EditText native view when it loses focus.
+ */
+/* package */ class ReactTextInputBlurEvent extends Event {
+
+ private static final String EVENT_NAME = "topBlur";
+
+ public ReactTextInputBlurEvent(
+ int viewId,
+ long timestampMs) {
+ super(viewId, timestampMs);
+ }
+
+ @Override
+ public String getEventName() {
+ return EVENT_NAME;
+ }
+
+ @Override
+ public boolean canCoalesce() {
+ return false;
+ }
+
+ @Override
+ public void dispatch(RCTEventEmitter rctEventEmitter) {
+ rctEventEmitter.receiveEvent(getViewTag(), getEventName(), serializeEventData());
+ }
+
+ private WritableMap serializeEventData() {
+ WritableMap eventData = Arguments.createMap();
+ eventData.putInt("target", getViewTag());
+ return eventData;
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputEndEditingEvent.java b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputEndEditingEvent.java
new file mode 100644
index 00000000000000..ff99d68f7f9abe
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputEndEditingEvent.java
@@ -0,0 +1,56 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+package com.facebook.react.views.textinput;
+
+import com.facebook.react.bridge.Arguments;
+import com.facebook.react.bridge.WritableMap;
+import com.facebook.react.uimanager.events.Event;
+import com.facebook.react.uimanager.events.RCTEventEmitter;
+
+/**
+ * Event emitted by EditText native view when text editing ends,
+ * because of the user leaving the text input.
+ */
+class ReactTextInputEndEditingEvent extends Event {
+
+ private static final String EVENT_NAME = "topEndEditing";
+
+ private String mText;
+
+ public ReactTextInputEndEditingEvent(
+ int viewId,
+ long timestampMs,
+ String text) {
+ super(viewId, timestampMs);
+ mText = text;
+ }
+
+ @Override
+ public String getEventName() {
+ return EVENT_NAME;
+ }
+
+ @Override
+ public boolean canCoalesce() {
+ return false;
+ }
+
+ @Override
+ public void dispatch(RCTEventEmitter rctEventEmitter) {
+ rctEventEmitter.receiveEvent(getViewTag(), getEventName(), serializeEventData());
+ }
+
+ private WritableMap serializeEventData() {
+ WritableMap eventData = Arguments.createMap();
+ eventData.putInt("target", getViewTag());
+ eventData.putString("text", mText);
+ return eventData;
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputEvent.java b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputEvent.java
new file mode 100644
index 00000000000000..f2cbc8b9143f86
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputEvent.java
@@ -0,0 +1,72 @@
+/**
+ * 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.views.textinput;
+
+import com.facebook.react.bridge.Arguments;
+import com.facebook.react.bridge.WritableMap;
+import com.facebook.react.uimanager.events.Event;
+import com.facebook.react.uimanager.events.RCTEventEmitter;
+
+/**
+ * Event emitted by EditText native view when text changes.
+ */
+/* package */ class ReactTextInputEvent extends Event {
+
+ public static final String EVENT_NAME = "topTextInput";
+
+ private String mText;
+ private String mPreviousText;
+ private int mRangeStart;
+ private int mRangeEnd;
+
+ public ReactTextInputEvent(
+ int viewId,
+ long timestampMs,
+ String text,
+ String previousText,
+ int rangeStart,
+ int rangeEnd) {
+ super(viewId, timestampMs);
+ mText = text;
+ mPreviousText = previousText;
+ mRangeStart = rangeStart;
+ mRangeEnd = rangeEnd;
+ }
+
+ @Override
+ public String getEventName() {
+ return EVENT_NAME;
+ }
+
+ @Override
+ public boolean canCoalesce() {
+ // We don't want to miss any textinput event, as event data is incremental.
+ return false;
+ }
+
+ @Override
+ public void dispatch(RCTEventEmitter rctEventEmitter) {
+ rctEventEmitter.receiveEvent(getViewTag(), getEventName(), serializeEventData());
+ }
+
+ private WritableMap serializeEventData() {
+ WritableMap eventData = Arguments.createMap();
+ WritableMap range = Arguments.createMap();
+ range.putDouble("start", mRangeStart);
+ range.putDouble("end", mRangeEnd);
+
+ eventData.putString("text", mText);
+ eventData.putString("previousText", mPreviousText);
+ eventData.putMap("range", range);
+
+ eventData.putInt("target", getViewTag());
+ return eventData;
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputFocusEvent.java b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputFocusEvent.java
new file mode 100644
index 00000000000000..e593e851e872af
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputFocusEvent.java
@@ -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.views.textinput;
+
+import com.facebook.react.bridge.Arguments;
+import com.facebook.react.bridge.WritableMap;
+import com.facebook.react.uimanager.events.Event;
+import com.facebook.react.uimanager.events.RCTEventEmitter;
+
+/**
+ * Event emitted by EditText native view when it receives focus.
+ */
+/* package */ class ReactTextInputFocusEvent extends Event {
+
+ private static final String EVENT_NAME = "topFocus";
+
+ public ReactTextInputFocusEvent(
+ int viewId,
+ long timestampMs) {
+ super(viewId, timestampMs);
+ }
+
+ @Override
+ public String getEventName() {
+ return EVENT_NAME;
+ }
+
+ @Override
+ public boolean canCoalesce() {
+ return false;
+ }
+
+ @Override
+ public void dispatch(RCTEventEmitter rctEventEmitter) {
+ rctEventEmitter.receiveEvent(getViewTag(), getEventName(), serializeEventData());
+ }
+
+ private WritableMap serializeEventData() {
+ WritableMap eventData = Arguments.createMap();
+ eventData.putInt("target", getViewTag());
+ return eventData;
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java
new file mode 100644
index 00000000000000..d2f4251ebb86b3
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java
@@ -0,0 +1,445 @@
+/**
+ * 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.views.textinput;
+
+import javax.annotation.Nullable;
+
+import java.util.Map;
+
+import android.graphics.PorterDuff;
+import android.os.SystemClock;
+import android.text.Editable;
+import android.text.InputType;
+import android.text.TextWatcher;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.inputmethod.EditorInfo;
+import android.widget.TextView;
+
+import com.facebook.infer.annotation.Assertions;
+import com.facebook.react.bridge.JSApplicationCausedNativeException;
+import com.facebook.react.bridge.ReactContext;
+import com.facebook.react.bridge.ReadableArray;
+import com.facebook.react.common.MapBuilder;
+import com.facebook.react.uimanager.BaseViewPropertyApplicator;
+import com.facebook.react.uimanager.CSSColorUtil;
+import com.facebook.react.uimanager.CatalystStylesDiffMap;
+import com.facebook.react.uimanager.PixelUtil;
+import com.facebook.react.uimanager.ThemedReactContext;
+import com.facebook.react.uimanager.UIManagerModule;
+import com.facebook.react.uimanager.UIProp;
+import com.facebook.react.uimanager.ViewDefaults;
+import com.facebook.react.uimanager.ViewManager;
+import com.facebook.react.uimanager.ViewProps;
+import com.facebook.react.uimanager.events.EventDispatcher;
+import com.facebook.react.views.text.DefaultStyleValuesUtil;
+
+/**
+ * Manages instances of TextInput.
+ */
+public class ReactTextInputManager extends ViewManager {
+
+ /* package */ static final String REACT_CLASS = "AndroidTextInput";
+
+ private static final int FOCUS_TEXT_INPUT = 1;
+ private static final int BLUR_TEXT_INPUT = 2;
+
+ @UIProp(UIProp.Type.NUMBER)
+ public static final String PROP_FONT_SIZE = ViewProps.FONT_SIZE;
+ @UIProp(UIProp.Type.BOOLEAN)
+ public static final String PROP_TEXT_INPUT_AUTO_CORRECT = "autoCorrect";
+ @UIProp(UIProp.Type.NUMBER)
+ public static final String PROP_TEXT_INPUT_AUTO_CAPITALIZE = "autoCapitalize";
+ @UIProp(UIProp.Type.NUMBER)
+ public static final String PROP_TEXT_ALIGN = "textAlign";
+ @UIProp(UIProp.Type.NUMBER)
+ public static final String PROP_TEXT_ALIGN_VERTICAL = "textAlignVertical";
+ @UIProp(UIProp.Type.STRING)
+ public static final String PROP_TEXT_INPUT_HINT = "placeholder";
+ @UIProp(UIProp.Type.STRING)
+ public static final String PROP_TEXT_INPUT_HINT_COLOR = "placeholderTextColor";
+ @UIProp(UIProp.Type.NUMBER)
+ public static final String PROP_TEXT_INPUT_NUMLINES = ViewProps.NUMBER_OF_LINES;
+ @UIProp(UIProp.Type.BOOLEAN)
+ public static final String PROP_TEXT_INPUT_MULTILINE = "multiline";
+ @UIProp(UIProp.Type.STRING)
+ public static final String PROP_TEXT_INPUT_KEYBOARD_TYPE = "keyboardType";
+ @UIProp(UIProp.Type.BOOLEAN)
+ public static final String PROP_TEXT_INPUT_PASSWORD = "password";
+ @UIProp(UIProp.Type.BOOLEAN)
+ public static final String PROP_TEXT_INPUT_EDITABLE = "editable";
+ @UIProp(UIProp.Type.STRING)
+ public static final String PROP_TEXT_INPUT_UNDERLINE_COLOR = "underlineColorAndroid";
+
+ private static final String KEYBOARD_TYPE_EMAIL_ADDRESS = "email-address";
+ private static final String KEYBOARD_TYPE_NUMERIC = "numeric";
+
+ @Override
+ public String getName() {
+ return REACT_CLASS;
+ }
+
+ @Override
+ public ReactEditText createViewInstance(ThemedReactContext context) {
+ ReactEditText editText = new ReactEditText(context);
+ int inputType = editText.getInputType();
+ editText.setInputType(inputType & (~InputType.TYPE_TEXT_FLAG_MULTI_LINE));
+ editText.setTextSize(
+ TypedValue.COMPLEX_UNIT_PX,
+ (int) Math.ceil(PixelUtil.toPixelFromSP(ViewDefaults.FONT_SIZE_SP)));
+ return editText;
+ }
+
+ @Override
+ public ReactTextInputShadowNode createCSSNodeInstance() {
+ return new ReactTextInputShadowNode();
+ }
+
+ @Nullable
+ @Override
+ public Map getExportedCustomBubblingEventTypeConstants() {
+ return MapBuilder.builder()
+ .put(
+ "topSubmitEditing",
+ MapBuilder.of(
+ "phasedRegistrationNames",
+ MapBuilder.of(
+ "bubbled", "onSubmitEditing", "captured", "onSubmitEditingCapture")))
+ .put(
+ "topEndEditing",
+ MapBuilder.of(
+ "phasedRegistrationNames",
+ MapBuilder.of("bubbled", "onEndEditing", "captured", "onEndEditingCapture")))
+ .put(
+ "topTextInput",
+ MapBuilder.of(
+ "phasedRegistrationNames",
+ MapBuilder.of("bubbled", "onTextInput", "captured", "onTextInputCapture")))
+ .put(
+ "topFocus",
+ MapBuilder.of(
+ "phasedRegistrationNames",
+ MapBuilder.of("bubbled", "onFocus", "captured", "onFocusCapture")))
+ .put(
+ "topBlur",
+ MapBuilder.of(
+ "phasedRegistrationNames",
+ MapBuilder.of("bubbled", "onBlur", "captured", "onBlurCapture")))
+ .build();
+ }
+
+ @Override
+ public @Nullable Map getCommandsMap() {
+ return MapBuilder.of("focusTextInput", FOCUS_TEXT_INPUT, "blurTextInput", BLUR_TEXT_INPUT);
+ }
+
+ @Override
+ public void receiveCommand(
+ ReactEditText reactEditText,
+ int commandId,
+ @Nullable ReadableArray args) {
+ switch (commandId) {
+ case FOCUS_TEXT_INPUT:
+ reactEditText.requestFocusFromJS();
+ break;
+ case BLUR_TEXT_INPUT:
+ reactEditText.clearFocusFromJS();
+ break;
+ }
+ }
+
+ @Override
+ public void updateExtraData(ReactEditText view, Object extraData) {
+ if (extraData instanceof float[]) {
+ float[] padding = (float[]) extraData;
+
+ view.setPadding(
+ (int) Math.ceil(padding[0]),
+ (int) Math.ceil(padding[1]),
+ (int) Math.ceil(padding[2]),
+ (int) Math.ceil(padding[3]));
+ } else if (extraData instanceof ReactTextUpdate) {
+ view.maybeSetText((ReactTextUpdate) extraData);
+ }
+ }
+
+ @Override
+ public void updateView(ReactEditText view, CatalystStylesDiffMap props) {
+ BaseViewPropertyApplicator.applyCommonViewProperties(view, props);
+
+ if (props.hasKey(PROP_FONT_SIZE)) {
+ float textSize = props.getFloat(PROP_FONT_SIZE, ViewDefaults.FONT_SIZE_SP);
+ view.setTextSize(
+ TypedValue.COMPLEX_UNIT_PX,
+ (int) Math.ceil(PixelUtil.toPixelFromSP(textSize)));
+ }
+
+ //Prevents flickering color while waiting for JS update.
+ if (props.hasKey(ViewProps.COLOR)) {
+ final String colorStr = props.getString(ViewProps.COLOR);
+ if (colorStr != null) {
+ final int color = CSSColorUtil.getColor(colorStr);
+ view.setTextColor(color);
+ } else {
+ view.setTextColor(DefaultStyleValuesUtil.getDefaultTextColor(view.getContext()));
+ }
+ }
+
+ if (props.hasKey(PROP_TEXT_INPUT_HINT)) {
+ view.setHint(props.getString(PROP_TEXT_INPUT_HINT));
+ }
+
+ if (props.hasKey(PROP_TEXT_INPUT_HINT_COLOR)) {
+ final String colorStr = props.getString(PROP_TEXT_INPUT_HINT_COLOR);
+ if (colorStr != null) {
+ final int color = CSSColorUtil.getColor(colorStr);
+ view.setHintTextColor(color);
+ } else {
+ view.setHintTextColor(DefaultStyleValuesUtil.getDefaultTextColorHint(view.getContext()));
+ // We need to invalidate in order to force EditText to update hint color.
+ // see updateTextColors() method in TextView.java
+ view.invalidate();
+ }
+ }
+
+ if (props.hasKey(PROP_TEXT_INPUT_UNDERLINE_COLOR)) {
+ String colorStr = props.getString(PROP_TEXT_INPUT_UNDERLINE_COLOR);
+ if (colorStr != null) {
+ int color = CSSColorUtil.getColor(colorStr);
+ view.getBackground().setColorFilter(color, PorterDuff.Mode.SRC_IN);
+ } else {
+ view.getBackground().clearColorFilter();
+ }
+ }
+
+ if (props.hasKey(PROP_TEXT_ALIGN)) {
+ int gravityHorizontal = props.getInt(PROP_TEXT_ALIGN, 0);
+ view.setGravityHorizontal(gravityHorizontal);
+ }
+
+ if (props.hasKey(PROP_TEXT_ALIGN_VERTICAL)) {
+ int gravityVertical = props.getInt(PROP_TEXT_ALIGN_VERTICAL, 0);
+ view.setGravityVertical(gravityVertical);
+ }
+
+ if (props.hasKey(PROP_TEXT_INPUT_EDITABLE)) {
+ if (props.getBoolean(PROP_TEXT_INPUT_EDITABLE, true)) {
+ view.setEnabled(true);
+ } else {
+ view.setEnabled(false);
+ }
+ }
+
+ // newInputType will collect all content attributes that have to be set in the InputText.
+ int newInputType = view.getInputType();
+
+ if (props.hasKey(PROP_TEXT_INPUT_AUTO_CORRECT)) {
+ // clear auto correct flags
+ newInputType
+ &= ~(InputType.TYPE_TEXT_FLAG_AUTO_CORRECT | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
+ if (props.getBoolean(PROP_TEXT_INPUT_AUTO_CORRECT, false)) {
+ newInputType |= InputType.TYPE_TEXT_FLAG_AUTO_CORRECT;
+ } else if (!props.isNull(PROP_TEXT_INPUT_AUTO_CORRECT)) {
+ newInputType |= InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS;
+ }
+ }
+
+ if (props.hasKey(PROP_TEXT_INPUT_MULTILINE)) {
+ if (props.getBoolean(PROP_TEXT_INPUT_MULTILINE, false)) {
+ newInputType |= InputType.TYPE_TEXT_FLAG_MULTI_LINE;
+ } else {
+ newInputType &= ~InputType.TYPE_TEXT_FLAG_MULTI_LINE;
+ }
+ }
+
+ if (props.hasKey(PROP_TEXT_INPUT_KEYBOARD_TYPE)) {
+ // reset keyboard type defaults
+ newInputType = newInputType &
+ ~InputType.TYPE_CLASS_NUMBER &
+ ~InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS;
+
+ String keyboardType = props.getString(PROP_TEXT_INPUT_KEYBOARD_TYPE);
+ if (KEYBOARD_TYPE_NUMERIC.equalsIgnoreCase(keyboardType)) {
+ newInputType |= InputType.TYPE_CLASS_NUMBER;
+ } else if (KEYBOARD_TYPE_EMAIL_ADDRESS.equalsIgnoreCase(keyboardType)) {
+ newInputType |= InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS;
+ }
+ }
+
+ if (props.hasKey(PROP_TEXT_INPUT_PASSWORD)) {
+ if (props.getBoolean(PROP_TEXT_INPUT_PASSWORD, false)) {
+ newInputType |= InputType.TYPE_TEXT_VARIATION_PASSWORD;
+ } else {
+ newInputType &= ~InputType.TYPE_TEXT_VARIATION_PASSWORD;
+ }
+ }
+
+ if (props.hasKey(PROP_TEXT_INPUT_AUTO_CAPITALIZE)) {
+ // clear auto capitalization flags
+ newInputType &= ~(
+ InputType.TYPE_TEXT_FLAG_CAP_SENTENCES |
+ InputType.TYPE_TEXT_FLAG_CAP_WORDS |
+ InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS);
+ int autoCapitalize = props.getInt(PROP_TEXT_INPUT_AUTO_CAPITALIZE, InputType.TYPE_CLASS_TEXT);
+
+ switch (autoCapitalize) {
+ case InputType.TYPE_TEXT_FLAG_CAP_SENTENCES:
+ case InputType.TYPE_TEXT_FLAG_CAP_WORDS:
+ case InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS:
+ case InputType.TYPE_CLASS_TEXT:
+ newInputType |= autoCapitalize;
+ break;
+ default:
+ throw new
+ JSApplicationCausedNativeException("Invalid autoCapitalize value: " + autoCapitalize);
+ }
+ }
+
+ if (view.getInputType() != newInputType) {
+ view.setInputType(newInputType);
+ }
+
+ if (props.hasKey(PROP_TEXT_INPUT_NUMLINES)) {
+ view.setLines(props.getInt(PROP_TEXT_INPUT_NUMLINES, 1));
+ }
+ }
+
+ private class ReactTextInputTextWatcher implements TextWatcher {
+
+ private EventDispatcher mEventDispatcher;
+ private ReactEditText mEditText;
+ private String mPreviousText;
+
+ public ReactTextInputTextWatcher(
+ final ReactContext reactContext,
+ final ReactEditText editText) {
+ mEventDispatcher = reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher();
+ mEditText = editText;
+ mPreviousText = null;
+ }
+
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ // Incoming charSequence gets mutated before onTextChanged() is invoked
+ mPreviousText = s.toString();
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ // Rearranging the text (i.e. changing between singleline and multiline attributes) can
+ // also trigger onTextChanged, call the event in JS only when the text actually changed
+ if (count > 0 || before > 0) {
+ Assertions.assertNotNull(mPreviousText);
+
+ int contentWidth = mEditText.getWidth();
+ int contentHeight = mEditText.getHeight();
+
+ // Use instead size of text content within EditText when available
+ if (mEditText.getLayout() != null) {
+ contentWidth = mEditText.getCompoundPaddingLeft() + mEditText.getLayout().getWidth() +
+ mEditText.getCompoundPaddingRight();
+ contentHeight = mEditText.getCompoundPaddingTop() + mEditText.getLayout().getHeight() +
+ mEditText.getCompoundPaddingTop();
+ }
+
+ // The event that contains the event counter and updates it must be sent first.
+ // TODO: t7936714 merge these events
+ mEventDispatcher.dispatchEvent(
+ new ReactTextChangedEvent(
+ mEditText.getId(),
+ SystemClock.uptimeMillis(),
+ s.toString(),
+ (int) PixelUtil.toDIPFromPixel(contentWidth),
+ (int) PixelUtil.toDIPFromPixel(contentHeight),
+ mEditText.incrementAndGetEventCounter()));
+
+ mEventDispatcher.dispatchEvent(
+ new ReactTextInputEvent(
+ mEditText.getId(),
+ SystemClock.uptimeMillis(),
+ count > 0 ? s.toString().substring(start, start + count) : "",
+ before > 0 ? mPreviousText.substring(start, start + before) : "",
+ start,
+ count > 0 ? start + count - 1 : start + before));
+ }
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ }
+ }
+
+ @Override
+ protected void addEventEmitters(
+ final ThemedReactContext reactContext,
+ final ReactEditText editText) {
+ editText.addTextChangedListener(new ReactTextInputTextWatcher(reactContext, editText));
+ editText.setOnFocusChangeListener(
+ new View.OnFocusChangeListener() {
+ public void onFocusChange(View v, boolean hasFocus) {
+ EventDispatcher eventDispatcher =
+ reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher();
+ if (hasFocus) {
+ eventDispatcher.dispatchEvent(
+ new ReactTextInputFocusEvent(
+ editText.getId(),
+ SystemClock.uptimeMillis()));
+ } else {
+ eventDispatcher.dispatchEvent(
+ new ReactTextInputBlurEvent(
+ editText.getId(),
+ SystemClock.uptimeMillis()));
+
+ eventDispatcher.dispatchEvent(
+ new ReactTextInputEndEditingEvent(
+ editText.getId(),
+ SystemClock.uptimeMillis(),
+ editText.getText().toString()));
+ }
+ }
+ });
+
+ editText.setOnEditorActionListener(
+ new TextView.OnEditorActionListener() {
+ @Override
+ public boolean onEditorAction(TextView v, int actionId, KeyEvent keyEvent) {
+ // Any 'Enter' action will do
+ if ((actionId & EditorInfo.IME_MASK_ACTION) > 0 ||
+ actionId == EditorInfo.IME_NULL) {
+ EventDispatcher eventDispatcher =
+ reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher();
+ eventDispatcher.dispatchEvent(
+ new ReactTextInputSubmitEditingEvent(
+ editText.getId(),
+ SystemClock.uptimeMillis(),
+ editText.getText().toString()));
+ }
+ return false;
+ }
+ });
+ }
+
+ @Override
+ public @Nullable Map getExportedViewConstants() {
+ return MapBuilder.of(
+ "TextAlign",
+ MapBuilder.of(
+ "start", Gravity.START,
+ "center", Gravity.CENTER_HORIZONTAL,
+ "end", Gravity.END),
+ "TextAlignVertical",
+ MapBuilder.of(
+ "top", Gravity.TOP,
+ "center", Gravity.CENTER_VERTICAL,
+ "bottom", Gravity.BOTTOM));
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputShadowNode.java b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputShadowNode.java
new file mode 100644
index 00000000000000..6052b644d9deb0
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputShadowNode.java
@@ -0,0 +1,147 @@
+/**
+ * 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.views.textinput;
+
+import javax.annotation.Nullable;
+
+import android.text.Spanned;
+import android.util.TypedValue;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.EditText;
+
+import com.facebook.csslayout.CSSNode;
+import com.facebook.csslayout.MeasureOutput;
+import com.facebook.csslayout.Spacing;
+import com.facebook.infer.annotation.Assertions;
+import com.facebook.react.uimanager.CatalystStylesDiffMap;
+import com.facebook.react.uimanager.PixelUtil;
+import com.facebook.react.uimanager.ThemedReactContext;
+import com.facebook.react.uimanager.UIViewOperationQueue;
+import com.facebook.react.uimanager.ViewDefaults;
+import com.facebook.react.uimanager.ViewProps;
+import com.facebook.react.views.text.ReactTextShadowNode;
+
+/* package */ class ReactTextInputShadowNode extends ReactTextShadowNode implements
+ CSSNode.MeasureFunction {
+
+ public static final String PROP_TEXT_INPUT_MOST_RECENT_EVENT_COUNT = "mostRecentEventCount";
+ private static final int MEASURE_SPEC = View.MeasureSpec.makeMeasureSpec(
+ ViewGroup.LayoutParams.WRAP_CONTENT,
+ View.MeasureSpec.UNSPECIFIED);
+
+ private @Nullable EditText mEditText;
+ private int mFontSize;
+ private @Nullable float[] mComputedPadding;
+ private int mJsEventCount = UNSET;
+ private int mNumLines = UNSET;
+
+ public ReactTextInputShadowNode() {
+ super(false);
+ mFontSize = (int) Math.ceil(PixelUtil.toPixelFromSP(ViewDefaults.FONT_SIZE_SP));
+ setMeasureFunction(this);
+ }
+
+ @Override
+ protected void setThemedContext(ThemedReactContext themedContext) {
+ super.setThemedContext(themedContext);
+
+ // TODO #7120264: cache this stuff better
+ mEditText = new EditText(getThemedContext());
+ // This is needed to fix an android bug since 4.4.3 which will throw an NPE in measure,
+ // setting the layoutParams fixes it: https://code.google.com/p/android/issues/detail?id=75877
+ mEditText.setLayoutParams(
+ new ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT));
+
+ setDefaultPadding(Spacing.LEFT, mEditText.getPaddingLeft());
+ setDefaultPadding(Spacing.TOP, mEditText.getPaddingTop());
+ setDefaultPadding(Spacing.RIGHT, mEditText.getPaddingRight());
+ setDefaultPadding(Spacing.BOTTOM, mEditText.getPaddingBottom());
+ mComputedPadding = spacingToFloatArray(getStylePadding());
+ }
+
+ @Override
+ public void measure(CSSNode node, float width, MeasureOutput measureOutput) {
+ // measure() should never be called before setThemedContext()
+ EditText editText = Assertions.assertNotNull(mEditText);
+
+ measureOutput.width = width;
+ editText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mFontSize);
+ mComputedPadding = spacingToFloatArray(getStylePadding());
+ editText.setPadding(
+ (int) Math.ceil(getStylePadding().get(Spacing.LEFT)),
+ (int) Math.ceil(getStylePadding().get(Spacing.TOP)),
+ (int) Math.ceil(getStylePadding().get(Spacing.RIGHT)),
+ (int) Math.ceil(getStylePadding().get(Spacing.BOTTOM)));
+
+ if (mNumLines != UNSET) {
+ editText.setLines(mNumLines);
+ }
+
+ editText.measure(MEASURE_SPEC, MEASURE_SPEC);
+ measureOutput.height = editText.getMeasuredHeight();
+ }
+
+ @Override
+ public void onBeforeLayout() {
+ // We don't have to measure the text within the text input.
+ return;
+ }
+
+ @Override
+ public void updateProperties(CatalystStylesDiffMap styles) {
+ super.updateProperties(styles);
+ if (styles.hasKey(ViewProps.FONT_SIZE)) {
+ float fontSize = styles.getFloat(ViewProps.FONT_SIZE, ViewDefaults.FONT_SIZE_SP);
+ mFontSize = (int) Math.ceil(PixelUtil.toPixelFromSP(fontSize));
+ }
+
+ if (styles.hasKey(PROP_TEXT_INPUT_MOST_RECENT_EVENT_COUNT)) {
+ mJsEventCount = styles.getInt(PROP_TEXT_INPUT_MOST_RECENT_EVENT_COUNT, 0);
+ }
+
+ if (styles.hasKey(ReactTextInputManager.PROP_TEXT_INPUT_NUMLINES)) {
+ mNumLines = styles.getInt(ReactTextInputManager.PROP_TEXT_INPUT_NUMLINES, UNSET);
+ }
+ }
+
+ @Override
+ public void onCollectExtraUpdates(UIViewOperationQueue uiViewOperationQueue) {
+ super.onCollectExtraUpdates(uiViewOperationQueue);
+ if (mComputedPadding != null) {
+ uiViewOperationQueue.enqueueUpdateExtraData(getReactTag(), mComputedPadding);
+ mComputedPadding = null;
+ }
+
+ if (mJsEventCount != UNSET) {
+ Spanned preparedSpannedText = fromTextCSSNode(this);
+ ReactTextUpdate reactTextUpdate = new ReactTextUpdate(preparedSpannedText, mJsEventCount);
+ uiViewOperationQueue.enqueueUpdateExtraData(getReactTag(), reactTextUpdate);
+ }
+ }
+
+ @Override
+ public void setPadding(int spacingType, float padding) {
+ super.setPadding(spacingType, padding);
+ mComputedPadding = spacingToFloatArray(getStylePadding());
+ markUpdated();
+ }
+
+ private static float[] spacingToFloatArray(Spacing spacing) {
+ return new float[] {
+ spacing.get(Spacing.LEFT),
+ spacing.get(Spacing.TOP),
+ spacing.get(Spacing.RIGHT),
+ spacing.get(Spacing.BOTTOM),
+ };
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputSubmitEditingEvent.java b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputSubmitEditingEvent.java
new file mode 100644
index 00000000000000..0b378dbb85cbe6
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputSubmitEditingEvent.java
@@ -0,0 +1,56 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+package com.facebook.react.views.textinput;
+
+import com.facebook.react.bridge.Arguments;
+import com.facebook.react.bridge.WritableMap;
+import com.facebook.react.uimanager.events.Event;
+import com.facebook.react.uimanager.events.RCTEventEmitter;
+
+/**
+ * Event emitted by EditText native view when the user submits the text.
+ */
+/* package */ class ReactTextInputSubmitEditingEvent
+ extends Event {
+
+ private static final String EVENT_NAME = "topSubmitEditing";
+
+ private String mText;
+
+ public ReactTextInputSubmitEditingEvent(
+ int viewId,
+ long timestampMs,
+ String text) {
+ super(viewId, timestampMs);
+ mText = text;
+ }
+
+ @Override
+ public String getEventName() {
+ return EVENT_NAME;
+ }
+
+ @Override
+ public boolean canCoalesce() {
+ return false;
+ }
+
+ @Override
+ public void dispatch(RCTEventEmitter rctEventEmitter) {
+ rctEventEmitter.receiveEvent(getViewTag(), getEventName(), serializeEventData());
+ }
+
+ private WritableMap serializeEventData() {
+ WritableMap eventData = Arguments.createMap();
+ eventData.putInt("target", getViewTag());
+ eventData.putString("text", mText);
+ return eventData;
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextUpdate.java b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextUpdate.java
new file mode 100644
index 00000000000000..fc6c443c4c4c72
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextUpdate.java
@@ -0,0 +1,35 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+package com.facebook.react.views.textinput;
+
+import android.text.Spanned;
+
+/**
+ * Class that contains the data needed for a Text Input text update.
+ * VisibleForTesting from {@link TextInputEventsTestCase}.
+ */
+public class ReactTextUpdate {
+
+ private final Spanned mText;
+ private final int mJsEventCounter;
+
+ public ReactTextUpdate(Spanned text, int jsEventCounter) {
+ mText = text;
+ mJsEventCounter = jsEventCounter;
+ }
+
+ public Spanned getText() {
+ return mText;
+ }
+
+ public int getJsEventCounter() {
+ return mJsEventCounter;
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/toolbar/ReactToolbarManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/toolbar/ReactToolbarManager.java
new file mode 100644
index 00000000000000..fb96b4478fb586
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/toolbar/ReactToolbarManager.java
@@ -0,0 +1,234 @@
+/**
+ * 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.views.toolbar;
+
+import javax.annotation.Nullable;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Color;
+import android.os.SystemClock;
+import android.support.v7.widget.Toolbar;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+
+import com.facebook.react.R;
+import com.facebook.react.bridge.ReadableArray;
+import com.facebook.react.bridge.ReadableMap;
+import com.facebook.react.bridge.WritableMap;
+import com.facebook.react.bridge.WritableNativeMap;
+import com.facebook.react.uimanager.CSSColorUtil;
+import com.facebook.react.uimanager.CatalystStylesDiffMap;
+import com.facebook.react.uimanager.ThemedReactContext;
+import com.facebook.react.uimanager.UIManagerModule;
+import com.facebook.react.uimanager.UIProp;
+import com.facebook.react.uimanager.events.EventDispatcher;
+import com.facebook.react.views.toolbar.events.ToolbarClickEvent;
+import com.facebook.react.uimanager.ViewGroupManager;
+
+/**
+ * Manages instances of Toolbar.
+ */
+public class ReactToolbarManager extends ViewGroupManager {
+
+ private static final String REACT_CLASS = "ToolbarAndroid";
+
+ @UIProp(UIProp.Type.STRING)
+ public static final String PROP_LOGO = "logo";
+ @UIProp(UIProp.Type.STRING)
+ public static final String PROP_NAV_ICON = "navIcon";
+ @UIProp(UIProp.Type.STRING)
+ public static final String PROP_SUBTITLE = "subtitle";
+ @UIProp(UIProp.Type.STRING)
+ public static final String PROP_SUBTITLE_COLOR = "subtitleColor";
+ @UIProp(UIProp.Type.STRING)
+ public static final String PROP_TITLE = "title";
+ @UIProp(UIProp.Type.STRING)
+ public static final String PROP_TITLE_COLOR = "titleColor";
+ @UIProp(UIProp.Type.ARRAY)
+ public static final String PROP_ACTIONS = "actions";
+
+ private static final String PROP_ACTION_ICON = "icon";
+ private static final String PROP_ACTION_SHOW = "show";
+ private static final String PROP_ACTION_SHOW_WITH_TEXT = "showWithText";
+ private static final String PROP_ACTION_TITLE = "title";
+
+ @Override
+ public String getName() {
+ return REACT_CLASS;
+ }
+
+ @Override
+ protected Toolbar createViewInstance(ThemedReactContext reactContext) {
+ return new Toolbar(reactContext);
+ }
+
+ @Override
+ public void updateView(Toolbar toolbar, CatalystStylesDiffMap props) {
+ super.updateView(toolbar, props);
+
+ int[] defaultColors = getDefaultColors(toolbar.getContext());
+ if (props.hasKey(PROP_SUBTITLE)) {
+ toolbar.setSubtitle(props.getString(PROP_SUBTITLE));
+ }
+ if (props.hasKey(PROP_SUBTITLE_COLOR)) {
+ String color = props.getString(PROP_SUBTITLE_COLOR);
+ if (color != null) {
+ toolbar.setSubtitleTextColor(CSSColorUtil.getColor(color));
+ } else {
+ toolbar.setSubtitleTextColor(defaultColors[1]);
+ }
+ }
+ if (props.hasKey(PROP_TITLE)) {
+ toolbar.setTitle(props.getString(PROP_TITLE));
+ }
+ if (props.hasKey(PROP_TITLE_COLOR)) {
+ String color = props.getString(PROP_TITLE_COLOR);
+ if (color != null) {
+ toolbar.setTitleTextColor(CSSColorUtil.getColor(color));
+ } else {
+ toolbar.setTitleTextColor(defaultColors[0]);
+ }
+ }
+ if (props.hasKey(PROP_NAV_ICON)) {
+ String navIcon = props.getString(PROP_NAV_ICON);
+ if (navIcon != null) {
+ toolbar.setNavigationIcon(getDrawableResourceByName(toolbar.getContext(), navIcon));
+ } else {
+ toolbar.setNavigationIcon(null);
+ }
+ }
+ if (props.hasKey(PROP_LOGO)) {
+ String logo = props.getString(PROP_LOGO);
+ if (logo != null) {
+ toolbar.setLogo(getDrawableResourceByName(toolbar.getContext(), logo));
+ } else {
+ toolbar.setLogo(null);
+ }
+ }
+ if (props.hasKey(PROP_ACTIONS)) {
+ setActions(toolbar, props.getArray(PROP_ACTIONS));
+ }
+ }
+
+ @Override
+ protected void addEventEmitters(final ThemedReactContext reactContext, final Toolbar view) {
+ final EventDispatcher mEventDispatcher = reactContext.getNativeModule(UIManagerModule.class)
+ .getEventDispatcher();
+ view.setNavigationOnClickListener(
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mEventDispatcher.dispatchEvent(
+ new ToolbarClickEvent(view.getId(), SystemClock.uptimeMillis(), -1));
+ }
+ });
+
+ view.setOnMenuItemClickListener(
+ new Toolbar.OnMenuItemClickListener() {
+ @Override
+ public boolean onMenuItemClick(MenuItem menuItem) {
+ mEventDispatcher.dispatchEvent(
+ new ToolbarClickEvent(
+ view.getId(),
+ SystemClock.uptimeMillis(),
+ menuItem.getOrder()));
+ return true;
+ }
+ });
+ }
+
+ @Override
+ public boolean needsCustomLayoutForChildren() {
+ return true;
+ }
+
+ private static void setActions(Toolbar toolbar, @Nullable ReadableArray actions) {
+ Menu menu = toolbar.getMenu();
+ menu.clear();
+ if (actions != null) {
+ for (int i = 0; i < actions.size(); i++) {
+ ReadableMap action = actions.getMap(i);
+ MenuItem item = menu.add(Menu.NONE, Menu.NONE, i, action.getString(PROP_ACTION_TITLE));
+ String icon = action.hasKey(PROP_ACTION_ICON) ? action.getString(PROP_ACTION_ICON) : null;
+ if (icon != null) {
+ item.setIcon(getDrawableResourceByName(toolbar.getContext(), icon));
+ }
+ String show = action.hasKey(PROP_ACTION_SHOW) ? action.getString(PROP_ACTION_SHOW) : null;
+ if (show != null) {
+ int showAsAction = MenuItem.SHOW_AS_ACTION_NEVER;
+ if ("always".equals(show)) {
+ showAsAction = MenuItem.SHOW_AS_ACTION_ALWAYS;
+ } else if ("ifRoom".equals(show)) {
+ showAsAction = MenuItem.SHOW_AS_ACTION_IF_ROOM;
+ }
+ if (action.hasKey(PROP_ACTION_SHOW_WITH_TEXT) &&
+ action.getBoolean(PROP_ACTION_SHOW_WITH_TEXT)) {
+ showAsAction = showAsAction | MenuItem.SHOW_AS_ACTION_WITH_TEXT;
+ }
+ item.setShowAsAction(showAsAction);
+ }
+ }
+ }
+ }
+
+ private static int[] getDefaultColors(Context context) {
+ Resources.Theme theme = context.getTheme();
+ TypedArray toolbarStyle = null;
+ TypedArray textAppearances = null;
+ TypedArray titleTextAppearance = null;
+ TypedArray subtitleTextAppearance = null;
+
+ try {
+ toolbarStyle = theme
+ .obtainStyledAttributes(new int[]{R.attr.toolbarStyle});
+ int toolbarStyleResId = toolbarStyle.getResourceId(0, 0);
+ textAppearances = theme.obtainStyledAttributes(
+ toolbarStyleResId, new int[]{
+ R.attr.titleTextAppearance,
+ R.attr.subtitleTextAppearance,
+ });
+ int titleTextAppearanceResId = textAppearances.getResourceId(0, 0);
+ int subtitleTextAppearanceResId = textAppearances.getResourceId(1, 0);
+
+ titleTextAppearance = theme
+ .obtainStyledAttributes(titleTextAppearanceResId, new int[]{android.R.attr.textColor});
+ subtitleTextAppearance = theme
+ .obtainStyledAttributes(subtitleTextAppearanceResId, new int[]{android.R.attr.textColor});
+
+ int titleTextColor = titleTextAppearance.getColor(0, Color.BLACK);
+ int subtitleTextColor = subtitleTextAppearance.getColor(0, Color.BLACK);
+
+ return new int[] {titleTextColor, subtitleTextColor};
+ } finally {
+ recycleQuietly(toolbarStyle);
+ recycleQuietly(textAppearances);
+ recycleQuietly(titleTextAppearance);
+ recycleQuietly(subtitleTextAppearance);
+ }
+ }
+
+ private static void recycleQuietly(@Nullable TypedArray style) {
+ if (style != null) {
+ style.recycle();
+ }
+ }
+
+ private static int getDrawableResourceByName(Context context, String name) {
+ name = name.toLowerCase().replace("-", "_");
+ return context.getResources().getIdentifier(
+ name,
+ "drawable",
+ context.getPackageName());
+ }
+
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/toolbar/events/ToolbarClickEvent.java b/ReactAndroid/src/main/java/com/facebook/react/views/toolbar/events/ToolbarClickEvent.java
new file mode 100644
index 00000000000000..4e6bfa0ff508b2
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/toolbar/events/ToolbarClickEvent.java
@@ -0,0 +1,51 @@
+/**
+ * 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.views.toolbar.events;
+
+import com.facebook.react.bridge.WritableMap;
+import com.facebook.react.bridge.WritableNativeMap;
+import com.facebook.react.uimanager.events.Event;
+import com.facebook.react.uimanager.events.RCTEventEmitter;
+
+/**
+ * Represents a click on the toolbar.
+ * Position is meaningful when the click happenned on a menu
+ */
+public class ToolbarClickEvent extends Event {
+
+ private static final String EVENT_NAME = "topSelect";
+ private final int position;
+
+ public ToolbarClickEvent(int viewId, long timestampMs, int position) {
+ super(viewId, timestampMs);
+ this.position = position;
+ }
+
+ public int getPosition() {
+ return position;
+ }
+
+ @Override
+ public String getEventName() {
+ return EVENT_NAME;
+ }
+
+ @Override
+ public boolean canCoalesce() {
+ return false;
+ }
+
+ @Override
+ public void dispatch(RCTEventEmitter rctEventEmitter) {
+ WritableMap event = new WritableNativeMap();
+ event.putInt("position", getPosition());
+ rctEventEmitter.receiveEvent(getViewTag(), getEventName(), event);
+ }
+
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/view/ColorUtil.java b/ReactAndroid/src/main/java/com/facebook/react/views/view/ColorUtil.java
new file mode 100644
index 00000000000000..a8efbd777d0d40
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/view/ColorUtil.java
@@ -0,0 +1,55 @@
+/**
+ * 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.views.view;
+
+import android.graphics.PixelFormat;
+
+/**
+ * Simple utility class for manipulating colors, based on Fresco's
+ * DrawableUtils (https://github.com/facebook/fresco).
+ * For a small helper like this, copying is simpler than adding
+ * a dependency on com.facebook.fresco.drawee.
+ */
+public class ColorUtil {
+
+ /**
+ * Multiplies the color with the given alpha.
+ * @param color color to be multiplied
+ * @param alpha value between 0 and 255
+ * @return multiplied color
+ */
+ public static int multiplyColorAlpha(int color, int alpha) {
+ if (alpha == 255) {
+ return color;
+ }
+ if (alpha == 0) {
+ return color & 0x00FFFFFF;
+ }
+ alpha = alpha + (alpha >> 7); // make it 0..256
+ int colorAlpha = color >>> 24;
+ int multipliedAlpha = colorAlpha * alpha >> 8;
+ return (multipliedAlpha << 24) | (color & 0x00FFFFFF);
+ }
+
+ /**
+ * Gets the opacity from a color. Inspired by Android ColorDrawable.
+ * @return opacity expressed by one of PixelFormat constants
+ */
+ public static int getOpacityFromColor(int color) {
+ int colorAlpha = color >>> 24;
+ if (colorAlpha == 255) {
+ return PixelFormat.OPAQUE;
+ } else if (colorAlpha == 0) {
+ return PixelFormat.TRANSPARENT;
+ } else {
+ return PixelFormat.TRANSLUCENT;
+ }
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactClippingViewGroup.java b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactClippingViewGroup.java
new file mode 100644
index 00000000000000..af73bda94c1064
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactClippingViewGroup.java
@@ -0,0 +1,63 @@
+/**
+ * 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.views.view;
+
+import android.graphics.Rect;
+import android.view.View;
+
+import com.facebook.react.uimanager.CatalystStylesDiffMap;
+
+/**
+ * Interface that should be implemented by {@link View} subclasses that support
+ * {@code removeClippedSubviews} property. When this property is set for the {@link ViewGroup}
+ * subclass it's responsible for detaching it's child views that are clipped by the view boundaries.
+ * Those view boundaries should be determined based on it's parent clipping area and current view's
+ * offset in parent and doesn't necessarily reflect the view visible area (in a sense of a value
+ * that {@link View#getGlobalVisibleRect} may return). In order to determine the clipping rect for
+ * current view helper method {@link ReactClippingViewGroupHelper#calculateClippingRect} can be used
+ * that takes into account parent view settings.
+ */
+public interface ReactClippingViewGroup {
+
+ /**
+ * Notify view that clipping area may have changed and it should recalculate the list of children
+ * that shold be attached/detached. This method should be called only when property
+ * {@code removeClippedSubviews} is set to {@code true} on a view.
+ *
+ * CAUTION: Views are responsible for calling {@link #updateClippingRect} on it's children. This
+ * should happen if child implement {@link ReactClippingViewGroup}, return true from
+ * {@link #getRemoveClippedSubviews} and clipping rect change of the current view may affect
+ * clipping rect of this child.
+ */
+ void updateClippingRect();
+
+ /**
+ * Get rectangular bounds to which view is currently clipped to. Called only on views that has set
+ * {@code removeCLippedSubviews} property value to {@code true}.
+ *
+ * @param outClippingRect output clipping rect should be written to this object.
+ */
+ void getClippingRect(Rect outClippingRect);
+
+ /**
+ * Sets property {@code removeClippedSubviews} as a result of property update in JS. Should be
+ * called only from @{link ViewManager#updateView} method.
+ *
+ * Helper method {@link ReactClippingViewGroupHelper#applyRemoveClippedSubviewsProperty} may be
+ * used by {@link ViewManager} subclass to apply this property based on property update map
+ * {@link CatalystStylesDiffMap}.
+ */
+ void setRemoveClippedSubviews(boolean removeClippedSubviews);
+
+ /**
+ * Get the current value of {@code removeClippedSubviews} property.
+ */
+ boolean getRemoveClippedSubviews();
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactClippingViewGroupHelper.java b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactClippingViewGroupHelper.java
new file mode 100644
index 00000000000000..67a2e9d453aba3
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactClippingViewGroupHelper.java
@@ -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.views.view;
+
+import javax.annotation.concurrent.NotThreadSafe;
+
+import android.graphics.Rect;
+import android.view.View;
+import android.view.ViewParent;
+
+import com.facebook.react.uimanager.CatalystStylesDiffMap;
+
+/**
+ * Provides implementation of common tasks for view and it's view manager supporting property
+ * {@code removeClippedSubviews}.
+ */
+@NotThreadSafe
+public class ReactClippingViewGroupHelper {
+
+ public static final String PROP_REMOVE_CLIPPED_SUBVIEWS = "removeClippedSubviews";
+
+ private static final Rect sHelperRect = new Rect();
+
+ /**
+ * Can be used by view that support {@code removeClippedSubviews} property to calculate area that
+ * given {@param view} should be clipped to based on the clipping rectangle of it's parent in
+ * case when parent is also set to clip it's children.
+ *
+ * @param view view that we want to calculate clipping rect for
+ * @param outputRect where the calculated rectangle will be written
+ */
+ public static void calculateClippingRect(View view, Rect outputRect) {
+ ViewParent parent = view.getParent();
+ if (parent == null) {
+ outputRect.setEmpty();
+ return;
+ } else if (parent instanceof ReactClippingViewGroup) {
+ ReactClippingViewGroup clippingViewGroup = (ReactClippingViewGroup) parent;
+ if (clippingViewGroup.getRemoveClippedSubviews()) {
+ clippingViewGroup.getClippingRect(sHelperRect);
+ sHelperRect.offset(-view.getLeft(), -view.getTop());
+ view.getDrawingRect(outputRect);
+ if (!outputRect.intersect(sHelperRect)) {
+ // rectangles does not intersect -> we should write empty rect to output
+ outputRect.setEmpty();
+ }
+ return;
+ }
+ }
+ view.getDrawingRect(outputRect);
+ }
+
+ /**
+ * Can be used by view's manager in {@link ViewManager#updateView} method to update property
+ * {@code removeClippedSubviews} in the view.
+ *
+ * @param view view instance passed to {@link ViewManager#updateView}
+ * @param props property map passed to {@link ViewManager#updateView}
+ */
+ public static void applyRemoveClippedSubviewsProperty(
+ ReactClippingViewGroup view,
+ CatalystStylesDiffMap props) {
+ if (props.hasKey(PROP_REMOVE_CLIPPED_SUBVIEWS)) {
+ view.setRemoveClippedSubviews(props.getBoolean(PROP_REMOVE_CLIPPED_SUBVIEWS, false));
+ }
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactDrawableHelper.java b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactDrawableHelper.java
new file mode 100644
index 00000000000000..503d01a6cd2e0c
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactDrawableHelper.java
@@ -0,0 +1,94 @@
+/**
+ * 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.views.view;
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.graphics.Color;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.RippleDrawable;
+import android.os.Build;
+import android.util.TypedValue;
+
+import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
+import com.facebook.react.bridge.ReadableMap;
+import com.facebook.react.bridge.SoftAssertions;
+import com.facebook.react.uimanager.CSSColorUtil;
+
+/**
+ * Utility class that helps with converting android drawable description used in JS to an actual
+ * instance of {@link Drawable}.
+ */
+/* package */ class ReactDrawableHelper {
+
+ private static final TypedValue sResolveOutValue = new TypedValue();
+
+ public static Drawable createDrawableFromJSDescription(
+ Context context,
+ ReadableMap drawableDescriptionDict) {
+ String type = drawableDescriptionDict.getString("type");
+ if ("ThemeAttrAndroid".equals(type)) {
+ String attr = drawableDescriptionDict.getString("attribute");
+ SoftAssertions.assertNotNull(attr);
+ int attrID = context.getResources().getIdentifier(attr, "attr", "android");
+ if (attrID == 0) {
+ throw new JSApplicationIllegalArgumentException("Attribute " + attr +
+ " couldn't be found in the resource list");
+ }
+ if (context.getTheme().resolveAttribute(attrID, sResolveOutValue, true)) {
+ final int version = Build.VERSION.SDK_INT;
+ if (version >= 21) {
+ return context.getResources()
+ .getDrawable(sResolveOutValue.resourceId, context.getTheme());
+ } else {
+ return context.getResources().getDrawable(sResolveOutValue.resourceId);
+ }
+ } else {
+ throw new JSApplicationIllegalArgumentException("Attribute " + attr +
+ " couldn't be resolved into a drawable");
+ }
+ } else if ("RippleAndroid".equals(type)) {
+ if (Build.VERSION.SDK_INT < 21) {
+ throw new JSApplicationIllegalArgumentException("Ripple drawable is not available on " +
+ "android API <21");
+ }
+ String colorName = drawableDescriptionDict.hasKey("color") ?
+ drawableDescriptionDict.getString("color") : null;
+ int color;
+ if (colorName != null) {
+ color = CSSColorUtil.getColor(colorName);
+ } else {
+ if (context.getTheme().resolveAttribute(
+ android.R.attr.colorControlHighlight,
+ sResolveOutValue,
+ true)) {
+ color = context.getResources().getColor(sResolveOutValue.resourceId);
+ } else {
+ throw new JSApplicationIllegalArgumentException("Attribute colorControlHighlight " +
+ "couldn't be resolved into a drawable");
+ }
+ }
+ Drawable mask = null;
+ if (!drawableDescriptionDict.hasKey("borderless") ||
+ drawableDescriptionDict.isNull("borderless") ||
+ !drawableDescriptionDict.getBoolean("borderless")) {
+ mask = new ColorDrawable(Color.WHITE);
+ }
+ ColorStateList colorStateList = new ColorStateList(
+ new int[][] {new int[]{}},
+ new int[] {color});
+ return new RippleDrawable(colorStateList, null, mask);
+ } else {
+ throw new JSApplicationIllegalArgumentException(
+ "Invalid type for android drawable: " + type);
+ }
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewBackgroundDrawable.java b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewBackgroundDrawable.java
new file mode 100644
index 00000000000000..4360c33fcfc9bf
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewBackgroundDrawable.java
@@ -0,0 +1,301 @@
+/**
+ * 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.views.view;
+
+import javax.annotation.Nullable;
+
+import java.util.Locale;
+
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ColorFilter;
+import android.graphics.DashPathEffect;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.PathEffect;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.drawable.Drawable;
+
+import com.facebook.react.common.annotations.VisibleForTesting;
+import com.facebook.csslayout.CSSConstants;
+import com.facebook.csslayout.FloatUtil;
+import com.facebook.csslayout.Spacing;
+
+/**
+ * A subclass of {@link Drawable} used for background of {@link ReactViewGroup}. It supports
+ * drawing background color and borders (including rounded borders) by providing a react friendly
+ * API (setter for each of those properties).
+ *
+ * The implementation tries to allocate as few objects as possible depending on which properties are
+ * set. E.g. for views with rounded background/borders we allocate {@code mPathForBorderRadius} and
+ * {@code mTempRectForBorderRadius}. In case when view have a rectangular borders we allocate
+ * {@code mBorderWidthResult} and similar. When only background color is set we won't allocate any
+ * extra/unnecessary objects.
+ */
+/* package */ class ReactViewBackgroundDrawable extends Drawable {
+
+ private static final int DEFAULT_BORDER_COLOR = Color.BLACK;
+
+ private static enum BorderStyle {
+ SOLID,
+ DASHED,
+ DOTTED;
+
+ public @Nullable PathEffect getPathEffect(float borderWidth) {
+ switch (this) {
+ case SOLID:
+ return null;
+
+ case DASHED:
+ return new DashPathEffect(
+ new float[] {borderWidth*3, borderWidth*3, borderWidth*3, borderWidth*3}, 0);
+
+ case DOTTED:
+ return new DashPathEffect(
+ new float[] {borderWidth, borderWidth, borderWidth, borderWidth}, 0);
+
+ default:
+ return null;
+ }
+ }
+ };
+
+ /* Value at Spacing.ALL index used for rounded borders, whole array used by rectangular borders */
+ private @Nullable Spacing mBorderWidth;
+ private @Nullable Spacing mBorderColor;
+ private @Nullable BorderStyle mBorderStyle;
+
+ /* Used for rounded border and rounded background */
+ private @Nullable PathEffect mPathEffectForBorderStyle;
+ private @Nullable Path mPathForBorderRadius;
+ private @Nullable RectF mTempRectForBorderRadius;
+ private boolean mNeedUpdatePathForBorderRadius = false;
+ private float mBorderRadius = CSSConstants.UNDEFINED;
+
+ /* Used by all types of background and for drawing borders */
+ private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ private int mColor = Color.TRANSPARENT;
+ private int mAlpha = 255;
+
+ @Override
+ public void draw(Canvas canvas) {
+ if (!CSSConstants.isUndefined(mBorderRadius) && mBorderRadius > 0) {
+ drawRoundedBackgroundWithBorders(canvas);
+ } else {
+ drawRectangularBackgroundWithBorders(canvas);
+ }
+ }
+
+ @Override
+ protected void onBoundsChange(Rect bounds) {
+ super.onBoundsChange(bounds);
+ mNeedUpdatePathForBorderRadius = true;
+ }
+
+ @Override
+ public void setAlpha(int alpha) {
+ if (alpha != mAlpha) {
+ mAlpha = alpha;
+ invalidateSelf();
+ }
+ }
+
+ @Override
+ public int getAlpha() {
+ return mAlpha;
+ }
+
+ @Override
+ public void setColorFilter(ColorFilter cf) {
+ // do nothing
+ }
+
+ @Override
+ public int getOpacity() {
+ return ColorUtil.getOpacityFromColor(ColorUtil.multiplyColorAlpha(mColor, mAlpha));
+ }
+
+ public void setBorderWidth(int position, float width) {
+ if (mBorderWidth == null) {
+ mBorderWidth = new Spacing();
+ }
+ if (!FloatUtil.floatsEqual(mBorderWidth.getRaw(position), width)) {
+ mBorderWidth.set(position, width);
+ if (position == Spacing.ALL) {
+ mNeedUpdatePathForBorderRadius = true;
+ }
+ invalidateSelf();
+ }
+ }
+
+ public void setBorderColor(int position, float color) {
+ if (mBorderColor == null) {
+ mBorderColor = new Spacing();
+ mBorderColor.setDefault(Spacing.LEFT, DEFAULT_BORDER_COLOR);
+ mBorderColor.setDefault(Spacing.TOP, DEFAULT_BORDER_COLOR);
+ mBorderColor.setDefault(Spacing.RIGHT, DEFAULT_BORDER_COLOR);
+ mBorderColor.setDefault(Spacing.BOTTOM, DEFAULT_BORDER_COLOR);
+ }
+ if (!FloatUtil.floatsEqual(mBorderColor.getRaw(position), color)) {
+ mBorderColor.set(position, color);
+ invalidateSelf();
+ }
+ }
+
+ public void setBorderStyle(@Nullable String style) {
+ BorderStyle borderStyle = style == null
+ ? null
+ : BorderStyle.valueOf(style.toUpperCase(Locale.US));
+ if (mBorderStyle != borderStyle) {
+ mBorderStyle = borderStyle;
+ mNeedUpdatePathForBorderRadius = true;
+ invalidateSelf();
+ }
+ }
+
+ public void setRadius(float radius) {
+ if (mBorderRadius != radius) {
+ mBorderRadius = radius;
+ invalidateSelf();
+ }
+ }
+
+ public void setColor(int color) {
+ mColor = color;
+ invalidateSelf();
+ }
+
+ @VisibleForTesting
+ public int getColor() {
+ return mColor;
+ }
+
+ private void drawRoundedBackgroundWithBorders(Canvas canvas) {
+ updatePath();
+ int useColor = ColorUtil.multiplyColorAlpha(mColor, mAlpha);
+ if ((useColor >>> 24) != 0) { // color is not transparent
+ mPaint.setColor(useColor);
+ mPaint.setStyle(Paint.Style.FILL);
+ canvas.drawPath(mPathForBorderRadius, mPaint);
+ }
+ // maybe draw borders?
+ float fullBorderWidth = getFullBorderWidth();
+ if (fullBorderWidth > 0) {
+ int borderColor = getFullBorderColor();
+ mPaint.setColor(ColorUtil.multiplyColorAlpha(borderColor, mAlpha));
+ mPaint.setStyle(Paint.Style.STROKE);
+ mPaint.setStrokeWidth(fullBorderWidth);
+ mPaint.setPathEffect(mPathEffectForBorderStyle);
+ canvas.drawPath(mPathForBorderRadius, mPaint);
+ }
+ }
+
+ private void updatePath() {
+ if (!mNeedUpdatePathForBorderRadius) {
+ return;
+ }
+ mNeedUpdatePathForBorderRadius = false;
+ if (mPathForBorderRadius == null) {
+ mPathForBorderRadius = new Path();
+ mTempRectForBorderRadius = new RectF();
+ }
+ mPathForBorderRadius.reset();
+ mTempRectForBorderRadius.set(getBounds());
+ float fullBorderWidth = getFullBorderWidth();
+ if (fullBorderWidth > 0) {
+ mTempRectForBorderRadius.inset(fullBorderWidth * 0.5f, fullBorderWidth * 0.5f);
+ }
+ mPathForBorderRadius.addRoundRect(
+ mTempRectForBorderRadius,
+ mBorderRadius,
+ mBorderRadius,
+ Path.Direction.CW);
+
+ mPathEffectForBorderStyle = mBorderStyle != null
+ ? mBorderStyle.getPathEffect(getFullBorderWidth())
+ : null;
+ }
+
+ /**
+ * For rounded borders we use default "borderWidth" property.
+ */
+ private float getFullBorderWidth() {
+ return (mBorderWidth != null && !CSSConstants.isUndefined(mBorderWidth.getRaw(Spacing.ALL))) ?
+ mBorderWidth.getRaw(Spacing.ALL) : 0f;
+ }
+
+ /**
+ * We use this method for getting color for rounded borders only similarly as for
+ * {@link #getFullBorderWidth}.
+ */
+ private int getFullBorderColor() {
+ return (mBorderColor != null && !CSSConstants.isUndefined(mBorderColor.getRaw(Spacing.ALL))) ?
+ (int) mBorderColor.getRaw(Spacing.ALL) : DEFAULT_BORDER_COLOR;
+ }
+
+ private void drawRectangularBackgroundWithBorders(Canvas canvas) {
+ int useColor = ColorUtil.multiplyColorAlpha(mColor, mAlpha);
+ if ((useColor >>> 24) != 0) { // color is not transparent
+ mPaint.setColor(useColor);
+ mPaint.setStyle(Paint.Style.FILL);
+ canvas.drawRect(getBounds(), mPaint);
+ }
+ // maybe draw borders?
+ if (getBorderWidth(Spacing.LEFT) > 0 || getBorderWidth(Spacing.TOP) > 0 ||
+ getBorderWidth(Spacing.RIGHT) > 0 || getBorderWidth(Spacing.BOTTOM) > 0) {
+
+ int borderLeft = getBorderWidth(Spacing.LEFT);
+ int borderTop = getBorderWidth(Spacing.TOP);
+ int borderRight = getBorderWidth(Spacing.RIGHT);
+ int borderBottom = getBorderWidth(Spacing.BOTTOM);
+ int colorLeft = getBorderColor(Spacing.LEFT);
+ int colorTop = getBorderColor(Spacing.TOP);
+ int colorRight = getBorderColor(Spacing.RIGHT);
+ int colorBottom = getBorderColor(Spacing.BOTTOM);
+
+ int width = getBounds().width();
+ int height = getBounds().height();
+
+ if (borderLeft > 0 && colorLeft != Color.TRANSPARENT) {
+ mPaint.setColor(colorLeft);
+ canvas.drawRect(0, borderTop, borderLeft, height - borderBottom, mPaint);
+ }
+
+ if (borderTop > 0 && colorTop != Color.TRANSPARENT) {
+ mPaint.setColor(colorTop);
+ canvas.drawRect(0, 0, width, borderTop, mPaint);
+ }
+
+ if (borderRight > 0 && colorRight != Color.TRANSPARENT) {
+ mPaint.setColor(colorRight);
+ canvas.drawRect(
+ width - borderRight,
+ borderTop,
+ width,
+ height - borderBottom,
+ mPaint);
+ }
+
+ if (borderBottom > 0 && colorBottom != Color.TRANSPARENT) {
+ mPaint.setColor(colorBottom);
+ canvas.drawRect(0, height - borderBottom, width, height, mPaint);
+ }
+ }
+ }
+
+ private int getBorderWidth(int position) {
+ return mBorderWidth != null ? Math.round(mBorderWidth.get(position)) : 0;
+ }
+
+ private int getBorderColor(int position) {
+ return mBorderColor != null ? (int) mBorderColor.get(position) : DEFAULT_BORDER_COLOR;
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java
new file mode 100644
index 00000000000000..2af6bb25426ded
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java
@@ -0,0 +1,487 @@
+/**
+ * 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.views.view;
+
+import javax.annotation.Nullable;
+
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.LayerDrawable;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.facebook.infer.annotation.Assertions;
+import com.facebook.react.common.annotations.VisibleForTesting;
+import com.facebook.react.touch.CatalystInterceptingViewGroup;
+import com.facebook.react.touch.OnInterceptTouchEventListener;
+import com.facebook.react.uimanager.MeasureSpecAssertions;
+import com.facebook.react.uimanager.PointerEvents;
+import com.facebook.react.uimanager.ReactPointerEventsView;
+
+/**
+ * Backing for a React View. Has support for borders, but since borders aren't common, lazy
+ * initializes most of the storage needed for them.
+ */
+public class ReactViewGroup extends ViewGroup implements
+ CatalystInterceptingViewGroup, ReactClippingViewGroup, ReactPointerEventsView {
+
+ private static final int ARRAY_CAPACITY_INCREMENT = 12;
+ private static final int DEFAULT_BACKGROUND_COLOR = Color.TRANSPARENT;
+ private static final LayoutParams sDefaultLayoutParam = new ViewGroup.LayoutParams(0, 0);
+ /* should only be used in {@link #updateClippingToRect} */
+ private static final Rect sHelperRect = new Rect();
+
+ /**
+ * This listener will be set for child views when removeClippedSubview property is enabled. When
+ * children layout is updated, it will call {@link #updateSubviewClipStatus} to notify parent
+ * view about that fact so that view can be attached/detached if necessary.
+ *
+ * TODO(7728005): Attach/detach views in batch - once per frame in case when multiple children
+ * update their layout.
+ */
+ private static final class ChildrenLayoutChangeListener implements OnLayoutChangeListener {
+
+ private final ReactViewGroup mParent;
+
+ private ChildrenLayoutChangeListener(ReactViewGroup parent) {
+ mParent = parent;
+ }
+
+ @Override
+ public void onLayoutChange(
+ View v,
+ int left,
+ int top,
+ int right,
+ int bottom,
+ int oldLeft,
+ int oldTop,
+ int oldRight,
+ int oldBottom) {
+ if (mParent.getRemoveClippedSubviews()) {
+ mParent.updateSubviewClipStatus(v);
+ }
+ }
+ }
+
+ // Following properties are here to support the option {@code removeClippedSubviews}. This is a
+ // temporary optimization/hack that is mainly applicable to the large list of images. The way
+ // it's implemented is that we store an additional array of children in view node. We selectively
+ // remove some of the views (detach) from it while still storing them in that additional array.
+ // We override all possible add methods for {@link ViewGroup} so that we can controll this process
+ // whenever the option is set. We also override {@link ViewGroup#getChildAt} and
+ // {@link ViewGroup#getChildCount} so those methods may return views that are not attached.
+ // This is risky but allows us to perform a correct cleanup in {@link NativeViewHierarchyManager}.
+ private boolean mRemoveClippedSubviews = false;
+ private @Nullable View[] mAllChildren = null;
+ private int mAllChildrenCount;
+ private @Nullable Rect mClippingRect;
+ private PointerEvents mPointerEvents = PointerEvents.AUTO;
+ private @Nullable ChildrenLayoutChangeListener mChildrenLayoutChangeListener;
+ private @Nullable ReactViewBackgroundDrawable mReactBackgroundDrawable;
+ private @Nullable OnInterceptTouchEventListener mOnInterceptTouchEventListener;
+ private boolean mNeedsOffscreenAlphaCompositing = false;
+
+ public ReactViewGroup(Context context) {
+ super(context);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ MeasureSpecAssertions.assertExplicitMeasureSpec(widthMeasureSpec, heightMeasureSpec);
+
+ setMeasuredDimension(
+ MeasureSpec.getSize(widthMeasureSpec),
+ MeasureSpec.getSize(heightMeasureSpec));
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ // No-op since UIManagerModule handles actually laying out children.
+ }
+
+ @Override
+ public void setBackgroundColor(int color) {
+ if (color == Color.TRANSPARENT) {
+ Drawable backgroundDrawble = getBackground();
+ if (mReactBackgroundDrawable != null && (backgroundDrawble instanceof LayerDrawable)) {
+ // extract translucent background portion from layerdrawable
+ super.setBackground(null);
+ LayerDrawable layerDrawable = (LayerDrawable) backgroundDrawble;
+ super.setBackground(layerDrawable.getDrawable(1));
+ } else if (backgroundDrawble instanceof ReactViewBackgroundDrawable) {
+ // mReactBackground is set for background
+ mReactBackgroundDrawable = null;
+ super.setBackground(null);
+ }
+ } else {
+ getOrCreateReactViewBackground().setColor(color);
+ }
+ }
+
+ @Override
+ public void setBackground(Drawable drawable) {
+ throw new UnsupportedOperationException(
+ "This method is not supported for ReactViewGroup instances");
+ }
+
+ public void setTranslucentBackgroundDrawable(@Nullable Drawable background) {
+ // it's required to call setBackground to null, as in some of the cases we may set new
+ // background to be a layer drawable that contains a drawable that has been previously setup
+ // as a background previously. This will not work correctly as the drawable callback logic is
+ // messed up in AOSP
+ super.setBackground(null);
+ if (mReactBackgroundDrawable != null && background != null) {
+ LayerDrawable layerDrawable =
+ new LayerDrawable(new Drawable[] {mReactBackgroundDrawable, background});
+ super.setBackground(layerDrawable);
+ } else if (background != null) {
+ super.setBackground(background);
+ }
+ }
+
+ @Override
+ public void setOnInterceptTouchEventListener(OnInterceptTouchEventListener listener) {
+ mOnInterceptTouchEventListener = listener;
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ if (mOnInterceptTouchEventListener != null &&
+ mOnInterceptTouchEventListener.onInterceptTouchEvent(this, ev)) {
+ return true;
+ }
+ // We intercept the touch event if the children are not supposed to receive it.
+ if (mPointerEvents == PointerEvents.NONE || mPointerEvents == PointerEvents.BOX_ONLY) {
+ return true;
+ }
+ return super.onInterceptTouchEvent(ev);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ // We do not accept the touch event if this view is not supposed to receive it.
+ if (mPointerEvents == PointerEvents.NONE || mPointerEvents == PointerEvents.BOX_NONE) {
+ return false;
+ }
+ // The root view always assumes any view that was tapped wants the touch
+ // and sends the event to JS as such.
+ // We don't need to do bubbling in native (it's already happening in JS).
+ // For an explanation of bubbling and capturing, see
+ // http://javascript.info/tutorial/bubbling-and-capturing#capturing
+ return true;
+ }
+
+ /**
+ * We override this to allow developers to determine whether they need offscreen alpha compositing
+ * or not. See the documentation of needsOffscreenAlphaCompositing in View.js.
+ */
+ @Override
+ public boolean hasOverlappingRendering() {
+ return mNeedsOffscreenAlphaCompositing;
+ }
+
+ /**
+ * See the documentation of needsOffscreenAlphaCompositing in View.js.
+ */
+ public void setNeedsOffscreenAlphaCompositing(boolean needsOffscreenAlphaCompositing) {
+ mNeedsOffscreenAlphaCompositing = needsOffscreenAlphaCompositing;
+ }
+
+ public void setBorderWidth(int position, float width) {
+ getOrCreateReactViewBackground().setBorderWidth(position, width);
+ }
+
+ public void setBorderColor(int position, float color) {
+ getOrCreateReactViewBackground().setBorderColor(position, color);
+ }
+
+ public void setBorderRadius(float borderRadius) {
+ getOrCreateReactViewBackground().setRadius(borderRadius);
+ }
+
+ public void setBorderStyle(@Nullable String style) {
+ getOrCreateReactViewBackground().setBorderStyle(style);
+ }
+
+ @Override
+ public void setRemoveClippedSubviews(boolean removeClippedSubviews) {
+ if (removeClippedSubviews == mRemoveClippedSubviews) {
+ return;
+ }
+ mRemoveClippedSubviews = removeClippedSubviews;
+ if (removeClippedSubviews) {
+ mClippingRect = new Rect();
+ ReactClippingViewGroupHelper.calculateClippingRect(this, mClippingRect);
+ mAllChildrenCount = getChildCount();
+ int initialSize = Math.max(12, mAllChildrenCount);
+ mAllChildren = new View[initialSize];
+ mChildrenLayoutChangeListener = new ChildrenLayoutChangeListener(this);
+ for (int i = 0; i < mAllChildrenCount; i++) {
+ View child = getChildAt(i);
+ mAllChildren[i] = child;
+ child.addOnLayoutChangeListener(mChildrenLayoutChangeListener);
+ }
+ updateClippingRect();
+ } else {
+ // Add all clipped views back, deallocate additional arrays, remove layoutChangeListener
+ Assertions.assertNotNull(mClippingRect);
+ Assertions.assertNotNull(mAllChildren);
+ Assertions.assertNotNull(mChildrenLayoutChangeListener);
+ for (int i = 0; i < mAllChildrenCount; i++) {
+ mAllChildren[i].removeOnLayoutChangeListener(mChildrenLayoutChangeListener);
+ }
+ getDrawingRect(mClippingRect);
+ updateClippingToRect(mClippingRect);
+ mAllChildren = null;
+ mClippingRect = null;
+ mAllChildrenCount = 0;
+ mChildrenLayoutChangeListener = null;
+ }
+ }
+
+ @Override
+ public boolean getRemoveClippedSubviews() {
+ return mRemoveClippedSubviews;
+ }
+
+ @Override
+ public void getClippingRect(Rect outClippingRect) {
+ outClippingRect.set(mClippingRect);
+ }
+
+ @Override
+ public void updateClippingRect() {
+ if (!mRemoveClippedSubviews) {
+ return;
+ }
+
+ Assertions.assertNotNull(mClippingRect);
+ Assertions.assertNotNull(mAllChildren);
+
+ ReactClippingViewGroupHelper.calculateClippingRect(this, mClippingRect);
+ updateClippingToRect(mClippingRect);
+ }
+
+ private void updateClippingToRect(Rect clippingRect) {
+ Assertions.assertNotNull(mAllChildren);
+ int clippedSoFar = 0;
+ for (int i = 0; i < mAllChildrenCount; i++) {
+ updateSubviewClipStatus(clippingRect, i, clippedSoFar);
+ if (mAllChildren[i].getParent() == null) {
+ clippedSoFar++;
+ }
+ }
+ }
+
+ private void updateSubviewClipStatus(Rect clippingRect, int idx, int clippedSoFar) {
+ View child = Assertions.assertNotNull(mAllChildren)[idx];
+ sHelperRect.set(child.getLeft(), child.getTop(), child.getRight(), child.getBottom());
+ boolean intersects = clippingRect
+ .intersects(sHelperRect.left, sHelperRect.top, sHelperRect.right, sHelperRect.bottom);
+ boolean needUpdateClippingRecursive = false;
+ if (!intersects && child.getParent() != null) {
+ // We can try saving on invalidate call here as the view that we remove is out of visible area
+ // therefore invalidation is not necessary.
+ super.removeViewsInLayout(idx - clippedSoFar, 1);
+ needUpdateClippingRecursive = true;
+ } else if (intersects && child.getParent() == null) {
+ super.addViewInLayout(child, idx - clippedSoFar, sDefaultLayoutParam, true);
+ invalidate();
+ needUpdateClippingRecursive = true;
+ } else if (intersects && !clippingRect.contains(sHelperRect)) {
+ // View is partially clipped.
+ needUpdateClippingRecursive = true;
+ }
+ if (needUpdateClippingRecursive) {
+ if (child instanceof ReactClippingViewGroup) {
+ // we don't use {@link sHelperRect} until the end of this loop, therefore it's safe
+ // to call this method that may write to the same {@link sHelperRect} object.
+ ReactClippingViewGroup clippingChild = (ReactClippingViewGroup) child;
+ if (clippingChild.getRemoveClippedSubviews()) {
+ clippingChild.updateClippingRect();
+ }
+ }
+ }
+ }
+
+ private void updateSubviewClipStatus(View subview) {
+ if (!mRemoveClippedSubviews || getParent() == null) {
+ return;
+ }
+
+ Assertions.assertNotNull(mClippingRect);
+ Assertions.assertNotNull(mAllChildren);
+
+ // do fast check whether intersect state changed
+ sHelperRect.set(subview.getLeft(), subview.getTop(), subview.getRight(), subview.getBottom());
+ boolean intersects = mClippingRect
+ .intersects(sHelperRect.left, sHelperRect.top, sHelperRect.right, sHelperRect.bottom);
+
+ // If it was intersecting before, should be attached to the parent
+ boolean oldIntersects = (subview.getParent() != null);
+
+ if (intersects != oldIntersects) {
+ int clippedSoFar = 0;
+ for (int i = 0; i < mAllChildrenCount; i++) {
+ if (mAllChildren[i] == subview) {
+ updateSubviewClipStatus(mClippingRect, i, clippedSoFar);
+ break;
+ }
+ if (mAllChildren[i].getParent() == null) {
+ clippedSoFar++;
+ }
+ }
+ }
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+ updateClippingRect();
+ }
+
+ @Override
+ public PointerEvents getPointerEvents() {
+ return mPointerEvents;
+ }
+
+ /*package*/ void setPointerEvents(PointerEvents pointerEvents) {
+ mPointerEvents = pointerEvents;
+ }
+
+ /*package*/ int getAllChildrenCount() {
+ return mAllChildrenCount;
+ }
+
+ /*package*/ View getChildAtWithSubviewClippingEnabled(int index) {
+ return Assertions.assertNotNull(mAllChildren)[index];
+ }
+
+ /*package*/ void addViewWithSubviewClippingEnabled(View child, int index) {
+ addViewWithSubviewClippingEnabled(child, index, sDefaultLayoutParam);
+ }
+
+ /*package*/ void addViewWithSubviewClippingEnabled(View child, int index, LayoutParams params) {
+ Assertions.assertCondition(mRemoveClippedSubviews);
+ Assertions.assertNotNull(mClippingRect);
+ Assertions.assertNotNull(mAllChildren);
+ addInArray(child, index);
+ // we add view as "clipped" and then run {@link #updateSubviewClipStatus} to conditionally
+ // attach it
+ int clippedSoFar = 0;
+ for (int i = 0; i < index; i++) {
+ if (mAllChildren[i].getParent() == null) {
+ clippedSoFar++;
+ }
+ }
+ updateSubviewClipStatus(mClippingRect, index, clippedSoFar);
+ child.addOnLayoutChangeListener(mChildrenLayoutChangeListener);
+ }
+
+ /*package*/ void removeViewWithSubviewClippingEnabled(View view) {
+ Assertions.assertCondition(mRemoveClippedSubviews);
+ Assertions.assertNotNull(mClippingRect);
+ Assertions.assertNotNull(mAllChildren);
+ view.removeOnLayoutChangeListener(mChildrenLayoutChangeListener);
+ int index = indexOfChildInAllChildren(view);
+ if (mAllChildren[index].getParent() != null) {
+ int clippedSoFar = 0;
+ for (int i = 0; i < index; i++) {
+ if (mAllChildren[i].getParent() == null) {
+ clippedSoFar++;
+ }
+ }
+ super.removeViewsInLayout(index - clippedSoFar, 1);
+ }
+ removeFromArray(index);
+ }
+
+ private int indexOfChildInAllChildren(View child) {
+ final int count = mAllChildrenCount;
+ final View[] children = Assertions.assertNotNull(mAllChildren);
+ for (int i = 0; i < count; i++) {
+ if (children[i] == child) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ private void addInArray(View child, int index) {
+ View[] children = Assertions.assertNotNull(mAllChildren);
+ final int count = mAllChildrenCount;
+ final int size = children.length;
+ if (index == count) {
+ if (size == count) {
+ mAllChildren = new View[size + ARRAY_CAPACITY_INCREMENT];
+ System.arraycopy(children, 0, mAllChildren, 0, size);
+ children = mAllChildren;
+ }
+ children[mAllChildrenCount++] = child;
+ } else if (index < count) {
+ if (size == count) {
+ mAllChildren = new View[size + ARRAY_CAPACITY_INCREMENT];
+ System.arraycopy(children, 0, mAllChildren, 0, index);
+ System.arraycopy(children, index, mAllChildren, index + 1, count - index);
+ children = mAllChildren;
+ } else {
+ System.arraycopy(children, index, children, index + 1, count - index);
+ }
+ children[index] = child;
+ mAllChildrenCount++;
+ } else {
+ throw new IndexOutOfBoundsException("index=" + index + " count=" + count);
+ }
+ }
+
+ // This method also sets the child's mParent to null
+ private void removeFromArray(int index) {
+ final View[] children = Assertions.assertNotNull(mAllChildren);
+ final int count = mAllChildrenCount;
+ if (index == count - 1) {
+ children[--mAllChildrenCount] = null;
+ } else if (index >= 0 && index < count) {
+ System.arraycopy(children, index + 1, children, index, count - index - 1);
+ children[--mAllChildrenCount] = null;
+ } else {
+ throw new IndexOutOfBoundsException();
+ }
+ }
+
+ @VisibleForTesting
+ public int getBackgroundColor() {
+ if (getBackground() != null) {
+ return ((ReactViewBackgroundDrawable) getBackground()).getColor();
+ }
+ return DEFAULT_BACKGROUND_COLOR;
+ }
+
+ private ReactViewBackgroundDrawable getOrCreateReactViewBackground() {
+ if (mReactBackgroundDrawable == null) {
+ mReactBackgroundDrawable = new ReactViewBackgroundDrawable();
+ Drawable backgroundDrawable = getBackground();
+ super.setBackground(null); // required so that drawable callback is cleared before we add the
+ // drawable back as a part of LayerDrawable
+ if (backgroundDrawable == null) {
+ super.setBackground(mReactBackgroundDrawable);
+ } else {
+ LayerDrawable layerDrawable =
+ new LayerDrawable(new Drawable[] {mReactBackgroundDrawable, backgroundDrawable});
+ super.setBackground(layerDrawable);
+ }
+ }
+ return mReactBackgroundDrawable;
+ }
+
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.java
new file mode 100644
index 00000000000000..a3decbb6efec8b
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.java
@@ -0,0 +1,222 @@
+/**
+ * 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.views.view;
+
+import javax.annotation.Nullable;
+
+import java.util.Locale;
+import java.util.Map;
+
+import android.os.Build;
+import android.view.View;
+
+import com.facebook.csslayout.CSSConstants;
+import com.facebook.csslayout.Spacing;
+import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
+import com.facebook.react.bridge.ReadableArray;
+import com.facebook.react.bridge.ReadableMap;
+import com.facebook.react.common.MapBuilder;
+import com.facebook.react.uimanager.BaseViewPropertyApplicator;
+import com.facebook.react.uimanager.CSSColorUtil;
+import com.facebook.react.uimanager.CatalystStylesDiffMap;
+import com.facebook.react.uimanager.PixelUtil;
+import com.facebook.react.uimanager.PointerEvents;
+import com.facebook.react.uimanager.ThemedReactContext;
+import com.facebook.react.uimanager.UIProp;
+import com.facebook.react.uimanager.ViewGroupManager;
+import com.facebook.react.uimanager.ViewProps;
+import com.facebook.react.common.annotations.VisibleForTesting;
+
+/**
+ * View manager for AndroidViews (plain React Views).
+ */
+public class ReactViewManager extends ViewGroupManager {
+
+ @VisibleForTesting
+ public static final String REACT_CLASS = ViewProps.VIEW_CLASS_NAME;
+
+ private static final int[] SPACING_TYPES = {
+ Spacing.ALL, Spacing.LEFT, Spacing.RIGHT, Spacing.TOP, Spacing.BOTTOM,
+ };
+ private static final String[] PROPS_BORDER_COLOR = {
+ "borderColor", "borderLeftColor", "borderRightColor", "borderTopColor", "borderBottomColor"
+ };
+ private static final int CMD_HOTSPOT_UPDATE = 1;
+ private static final int CMD_SET_PRESSED = 2;
+ private static final int[] sLocationBuf = new int[2];
+
+ @UIProp(UIProp.Type.STRING) public static final String PROP_ACCESSIBLE = "accessible";
+ @UIProp(UIProp.Type.NUMBER) public static final String PROP_BORDER_RADIUS = "borderRadius";
+ @UIProp(UIProp.Type.STRING) public static final String PROP_BORDER_STYLE = "borderStyle";
+ @UIProp(UIProp.Type.STRING) public static final String PROP_POINTER_EVENTS = "pointerEvents";
+ @UIProp(UIProp.Type.MAP) public static final String PROP_NATIVE_BG = "nativeBackgroundAndroid";
+
+ @Override
+ public String getName() {
+ return REACT_CLASS;
+ }
+
+ @Override
+ public ReactViewGroup createViewInstance(ThemedReactContext context) {
+ return new ReactViewGroup(context);
+ }
+
+ @Override
+ public Map getNativeProps() {
+ Map nativeProps = super.getNativeProps();
+ Map baseProps = BaseViewPropertyApplicator.getCommonProps();
+ for (Map.Entry entry : baseProps.entrySet()) {
+ nativeProps.put(entry.getKey(), entry.getValue());
+ }
+ for (int i = 0; i < SPACING_TYPES.length; i++) {
+ nativeProps.put(ViewProps.BORDER_WIDTHS[i], UIProp.Type.NUMBER);
+ nativeProps.put(PROPS_BORDER_COLOR[i], UIProp.Type.STRING);
+ }
+ return nativeProps;
+ }
+
+ @Override
+ public void updateView(ReactViewGroup view, CatalystStylesDiffMap props) {
+ super.updateView(view, props);
+ ReactClippingViewGroupHelper.applyRemoveClippedSubviewsProperty(view, props);
+
+ // Border widths
+ for (int i = 0; i < SPACING_TYPES.length; i++) {
+ String key = ViewProps.BORDER_WIDTHS[i];
+ if (props.hasKey(key)) {
+ float width = props.getFloat(key, CSSConstants.UNDEFINED);
+ if (!CSSConstants.isUndefined(width)) {
+ width = PixelUtil.toPixelFromDIP(width);
+ }
+ view.setBorderWidth(SPACING_TYPES[i], width);
+ }
+ }
+
+ // Border colors
+ for (int i = 0; i < SPACING_TYPES.length; i++) {
+ String key = PROPS_BORDER_COLOR[i];
+ if (props.hasKey(key)) {
+ String color = props.getString(key);
+ float colorFloat = color == null ? CSSConstants.UNDEFINED : CSSColorUtil.getColor(color);
+ view.setBorderColor(SPACING_TYPES[i], colorFloat);
+ }
+ }
+
+ // Border radius
+ if (props.hasKey(PROP_BORDER_RADIUS)) {
+ view.setBorderRadius(PixelUtil.toPixelFromDIP(props.getFloat(PROP_BORDER_RADIUS, 0.0f)));
+ }
+
+ if (props.hasKey(PROP_BORDER_STYLE)) {
+ view.setBorderStyle(props.getString(PROP_BORDER_STYLE));
+ }
+
+ if (props.hasKey(PROP_POINTER_EVENTS)) {
+ String pointerEventsStr = props.getString(PROP_POINTER_EVENTS);
+ if (pointerEventsStr != null) {
+ PointerEvents pointerEvents =
+ PointerEvents.valueOf(pointerEventsStr.toUpperCase(Locale.US).replace("-", "_"));
+ view.setPointerEvents(pointerEvents);
+ }
+ }
+
+ // Native background
+ if (props.hasKey(PROP_NATIVE_BG)) {
+ ReadableMap map = props.getMap(PROP_NATIVE_BG);
+ view.setTranslucentBackgroundDrawable(map == null ?
+ null : ReactDrawableHelper.createDrawableFromJSDescription(view.getContext(), map));
+ }
+
+ if (props.hasKey(PROP_ACCESSIBLE)) {
+ view.setFocusable(props.getBoolean(PROP_ACCESSIBLE, false));
+ }
+
+ if (props.hasKey(ViewProps.NEEDS_OFFSCREEN_ALPHA_COMPOSITING)) {
+ view.setNeedsOffscreenAlphaCompositing(
+ props.getBoolean(ViewProps.NEEDS_OFFSCREEN_ALPHA_COMPOSITING, false));
+ }
+ }
+
+ @Override
+ public Map getCommandsMap() {
+ return MapBuilder.of("hotspotUpdate", CMD_HOTSPOT_UPDATE, "setPressed", CMD_SET_PRESSED);
+ }
+
+ @Override
+ public void receiveCommand(ReactViewGroup root, int commandId, @Nullable ReadableArray args) {
+ switch (commandId) {
+ case CMD_HOTSPOT_UPDATE: {
+ if (args == null || args.size() != 2) {
+ throw new JSApplicationIllegalArgumentException(
+ "Illegal number of arguments for 'updateHotspot' command");
+ }
+ if (Build.VERSION.SDK_INT >= 21) {
+ root.getLocationOnScreen(sLocationBuf);
+ float x = PixelUtil.toPixelFromDIP(args.getDouble(0)) - sLocationBuf[0];
+ float y = PixelUtil.toPixelFromDIP(args.getDouble(1)) - sLocationBuf[1];
+ root.drawableHotspotChanged(x, y);
+ }
+ break;
+ }
+ case CMD_SET_PRESSED: {
+ if (args == null || args.size() != 1) {
+ throw new JSApplicationIllegalArgumentException(
+ "Illegal number of arguments for 'setPressed' command");
+ }
+ root.setPressed(args.getBoolean(0));
+ break;
+ }
+ }
+ }
+
+ @Override
+ public void addView(ReactViewGroup parent, View child, int index) {
+ boolean removeClippedSubviews = parent.getRemoveClippedSubviews();
+ if (removeClippedSubviews) {
+ parent.addViewWithSubviewClippingEnabled(child, index);
+ } else {
+ parent.addView(child, index);
+ }
+ }
+
+ @Override
+ public int getChildCount(ReactViewGroup parent) {
+ boolean removeClippedSubviews = parent.getRemoveClippedSubviews();
+ if (removeClippedSubviews) {
+ return parent.getAllChildrenCount();
+ } else {
+ return parent.getChildCount();
+ }
+ }
+
+ @Override
+ public View getChildAt(ReactViewGroup parent, int index) {
+ boolean removeClippedSubviews = parent.getRemoveClippedSubviews();
+ if (removeClippedSubviews) {
+ return parent.getChildAtWithSubviewClippingEnabled(index);
+ } else {
+ return parent.getChildAt(index);
+ }
+ }
+
+ @Override
+ public void removeView(ReactViewGroup parent, View child) {
+ boolean removeClippedSubviews = parent.getRemoveClippedSubviews();
+ if (removeClippedSubviews) {
+ if (child.getParent() != null) {
+ parent.removeView(child);
+ }
+ parent.removeViewWithSubviewClippingEnabled(child);
+ } else {
+ parent.removeView(child);
+ }
+ }
+
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/soloader/ApkSoSource.java b/ReactAndroid/src/main/java/com/facebook/soloader/ApkSoSource.java
new file mode 100644
index 00000000000000..eba1eeaf586503
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/soloader/ApkSoSource.java
@@ -0,0 +1,171 @@
+/**
+ * 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.soloader;
+
+import java.io.File;
+import java.io.IOException;
+import android.content.Context;
+
+import java.util.jar.JarFile;
+import java.util.jar.JarEntry;
+
+import java.util.regex.Pattern;
+import java.util.regex.Matcher;
+
+import android.os.Build;
+import android.system.Os;
+import android.system.ErrnoException;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Enumeration;
+
+import java.io.InputStream;
+import java.io.FileOutputStream;
+
+import android.util.Log;
+
+/**
+ * {@link SoSource} that extracts libraries from an APK to the filesystem.
+ */
+public class ApkSoSource extends DirectorySoSource {
+
+ private static final String TAG = SoLoader.TAG;
+ private static final boolean DEBUG = SoLoader.DEBUG;
+
+ /**
+ * Make a new ApkSoSource that extracts DSOs from our APK instead of relying on the system to do
+ * the extraction for us.
+ *
+ * @param context Application context
+ */
+ public ApkSoSource(Context context) throws IOException {
+ //
+ // Initialize a normal DirectorySoSource that will load from our extraction directory. At this
+ // point, the directory may be empty or contain obsolete libraries, but that's okay.
+ //
+
+ super(SysUtil.createLibsDirectory(context), DirectorySoSource.RESOLVE_DEPENDENCIES);
+
+ //
+ // Synchronize the contents of that directory with the library payload in our APK, deleting and
+ // extracting as needed.
+ //
+
+ try (JarFile apk = new JarFile(context.getApplicationInfo().publicSourceDir)) {
+ File libsDir = super.soDirectory;
+
+ if (DEBUG) {
+ Log.v(TAG, "synchronizing log directory: " + libsDir);
+ }
+
+ Map providedLibraries = findProvidedLibraries(apk);
+ try (FileLocker lock = SysUtil.lockLibsDirectory(context)) {
+ // Delete files in libsDir that we don't provide or that are out of date. Forget about any
+ // libraries that are up-to-date already so we don't unpack them below.
+ File extantFiles[] = libsDir.listFiles();
+ for (int i = 0; i < extantFiles.length; ++i) {
+ File extantFile = extantFiles[i];
+
+ if (DEBUG) {
+ Log.v(TAG, "considering libdir file: " + extantFile);
+ }
+
+ String name = extantFile.getName();
+ SoInfo so = providedLibraries.get(name);
+ boolean shouldDelete =
+ (so == null ||
+ so.entry.getSize() != extantFile.length() ||
+ so.entry.getTime() != extantFile.lastModified());
+ boolean upToDate = (so != null && !shouldDelete);
+
+ if (shouldDelete) {
+ if (DEBUG) {
+ Log.v(TAG, "deleting obsolete or unexpected file: " + extantFile);
+ }
+ SysUtil.deleteOrThrow(extantFile);
+ }
+
+ if (upToDate) {
+ if (DEBUG) {
+ Log.v(TAG, "found up-to-date library: " + extantFile);
+ }
+ providedLibraries.remove(name);
+ }
+ }
+
+ // Now extract any libraries left in providedLibraries; we removed all the up-to-date ones.
+ for (SoInfo so : providedLibraries.values()) {
+ JarEntry entry = so.entry;
+ try (InputStream is = apk.getInputStream(entry)) {
+ if (DEBUG) {
+ Log.v(TAG, "extracting library: " + so.soName);
+ }
+ SysUtil.reliablyCopyExecutable(
+ is,
+ new File(libsDir, so.soName),
+ entry.getSize(),
+ entry.getTime());
+ }
+
+ SysUtil.freeCopyBuffer();
+ }
+ }
+ }
+ }
+
+ /**
+ * Find the shared libraries provided in this APK and supported on this system. Each returend
+ * SoInfo points to the most preferred version of that library bundled with the given APK: for
+ * example, if we're on an armv7-a system and we have both arm and armv7-a versions of libfoo, the
+ * returned entry for libfoo points to the armv7-a version of libfoo.
+ *
+ * The caller owns the returned value and may mutate it.
+ *
+ * @param apk Opened application APK file
+ * @return Map of sonames to SoInfo instances
+ */
+ private static Map findProvidedLibraries(JarFile apk) {
+ // Subgroup 1: ABI. Subgroup 2: soname.
+ Pattern libPattern = Pattern.compile("^lib/([^/]+)/([^/]+\\.so)$");
+ HashMap providedLibraries = new HashMap<>();
+ String[] supportedAbis = SysUtil.getSupportedAbis();
+ Enumeration entries = apk.entries();
+ while (entries.hasMoreElements()) {
+ JarEntry entry = entries.nextElement();
+ Matcher m = libPattern.matcher(entry.getName());
+ if (m.matches()) {
+ String libraryAbi = m.group(1);
+ String soName = m.group(2);
+ int abiScore = SysUtil.findAbiScore(supportedAbis, libraryAbi);
+ if (abiScore >= 0) {
+ SoInfo so = providedLibraries.get(soName);
+ if (so == null || abiScore < so.abiScore) {
+ providedLibraries.put(soName, new SoInfo(soName, entry, abiScore));
+ }
+ }
+ }
+ }
+
+ return providedLibraries;
+ }
+
+ private static final class SoInfo {
+ public final String soName;
+ public final JarEntry entry;
+ public final int abiScore;
+
+ SoInfo(String soName, JarEntry entry, int abiScore) {
+ this.soName = soName;
+ this.entry = entry;
+ this.abiScore = abiScore;
+ }
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/soloader/DirectorySoSource.java b/ReactAndroid/src/main/java/com/facebook/soloader/DirectorySoSource.java
new file mode 100644
index 00000000000000..47cdb02320ddc6
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/soloader/DirectorySoSource.java
@@ -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.soloader;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * {@link SoSource} that finds shared libraries in a given directory.
+ */
+public class DirectorySoSource extends SoSource {
+
+ public static final int RESOLVE_DEPENDENCIES = 1;
+ public static final int ON_LD_LIBRARY_PATH = 2;
+
+ protected final File soDirectory;
+ private final int flags;
+
+ /**
+ * Make a new DirectorySoSource. If {@code flags} contains {@code RESOLVE_DEPENDENCIES},
+ * recursively load dependencies for shared objects loaded from this directory. (We shouldn't
+ * need to resolve dependencies for libraries loaded from system directories: the dynamic linker
+ * is smart enough to do it on its own there.)
+ */
+ public DirectorySoSource(File soDirectory, int flags) {
+ this.soDirectory = soDirectory;
+ this.flags = flags;
+ }
+
+ @Override
+ public int loadLibrary(String soName, int loadFlags) throws IOException {
+ File soFile = new File(soDirectory, soName);
+ if (!soFile.exists()) {
+ return LOAD_RESULT_NOT_FOUND;
+ }
+
+ if ((loadFlags & LOAD_FLAG_ALLOW_IMPLICIT_PROVISION) != 0 &&
+ (flags & ON_LD_LIBRARY_PATH) != 0) {
+ return LOAD_RESULT_IMPLICITLY_PROVIDED;
+ }
+
+ if ((flags & RESOLVE_DEPENDENCIES) != 0) {
+ String dependencies[] = MinElf.extract_DT_NEEDED(soFile);
+ for (int i = 0; i < dependencies.length; ++i) {
+ String dependency = dependencies[i];
+ if (dependency.startsWith("/")) {
+ continue;
+ }
+
+ SoLoader.loadLibraryBySoName(
+ dependency,
+ (loadFlags | LOAD_FLAG_ALLOW_IMPLICIT_PROVISION));
+ }
+ }
+
+ System.load(soFile.getAbsolutePath());
+ return LOAD_RESULT_LOADED;
+ }
+
+ @Override
+ public File unpackLibrary(String soName) throws IOException {
+ File soFile = new File(soDirectory, soName);
+ if (soFile.exists()) {
+ return soFile;
+ }
+
+ return null;
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/soloader/Elf32_Dyn.java b/ReactAndroid/src/main/java/com/facebook/soloader/Elf32_Dyn.java
new file mode 100644
index 00000000000000..a9ec0713dd1722
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/soloader/Elf32_Dyn.java
@@ -0,0 +1,14 @@
+/**
+ * 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.
+ */
+// AUTOMATICALLY GENERATED CODE. Regenerate with genstructs.sh.
+package com.facebook.soloader;
+public final class Elf32_Dyn {
+ public static final int d_tag = 0x0;
+ public static final int d_un = 0x4;
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/soloader/Elf32_Ehdr.java b/ReactAndroid/src/main/java/com/facebook/soloader/Elf32_Ehdr.java
new file mode 100644
index 00000000000000..a398ffe7898f3d
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/soloader/Elf32_Ehdr.java
@@ -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.
+ */
+// AUTOMATICALLY GENERATED CODE. Regenerate with genstructs.sh.
+package com.facebook.soloader;
+public final class Elf32_Ehdr {
+ public static final int e_ident = 0x0;
+ public static final int e_type = 0x10;
+ public static final int e_machine = 0x12;
+ public static final int e_version = 0x14;
+ public static final int e_entry = 0x18;
+ public static final int e_phoff = 0x1c;
+ public static final int e_shoff = 0x20;
+ public static final int e_flags = 0x24;
+ public static final int e_ehsize = 0x28;
+ public static final int e_phentsize = 0x2a;
+ public static final int e_phnum = 0x2c;
+ public static final int e_shentsize = 0x2e;
+ public static final int e_shnum = 0x30;
+ public static final int e_shstrndx = 0x32;
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/soloader/Elf32_Phdr.java b/ReactAndroid/src/main/java/com/facebook/soloader/Elf32_Phdr.java
new file mode 100644
index 00000000000000..95e2c27b29555b
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/soloader/Elf32_Phdr.java
@@ -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.
+ */
+// AUTOMATICALLY GENERATED CODE. Regenerate with genstructs.sh.
+package com.facebook.soloader;
+public final class Elf32_Phdr {
+ public static final int p_type = 0x0;
+ public static final int p_offset = 0x4;
+ public static final int p_vaddr = 0x8;
+ public static final int p_paddr = 0xc;
+ public static final int p_filesz = 0x10;
+ public static final int p_memsz = 0x14;
+ public static final int p_flags = 0x18;
+ public static final int p_align = 0x1c;
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/soloader/Elf32_Shdr.java b/ReactAndroid/src/main/java/com/facebook/soloader/Elf32_Shdr.java
new file mode 100644
index 00000000000000..35fc8599cd0880
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/soloader/Elf32_Shdr.java
@@ -0,0 +1,22 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+// AUTOMATICALLY GENERATED CODE. Regenerate with genstructs.sh.
+package com.facebook.soloader;
+public final class Elf32_Shdr {
+ public static final int sh_name = 0x0;
+ public static final int sh_type = 0x4;
+ public static final int sh_flags = 0x8;
+ public static final int sh_addr = 0xc;
+ public static final int sh_offset = 0x10;
+ public static final int sh_size = 0x14;
+ public static final int sh_link = 0x18;
+ public static final int sh_info = 0x1c;
+ public static final int sh_addralign = 0x20;
+ public static final int sh_entsize = 0x24;
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/soloader/Elf64_Dyn.java b/ReactAndroid/src/main/java/com/facebook/soloader/Elf64_Dyn.java
new file mode 100644
index 00000000000000..89f2ddbdf13e37
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/soloader/Elf64_Dyn.java
@@ -0,0 +1,14 @@
+/**
+ * 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.
+ */
+// AUTOMATICALLY GENERATED CODE. Regenerate with genstructs.sh.
+package com.facebook.soloader;
+public final class Elf64_Dyn {
+ public static final int d_tag = 0x0;
+ public static final int d_un = 0x8;
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/soloader/Elf64_Ehdr.java b/ReactAndroid/src/main/java/com/facebook/soloader/Elf64_Ehdr.java
new file mode 100644
index 00000000000000..4f6fa44ce28f7a
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/soloader/Elf64_Ehdr.java
@@ -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.
+ */
+// AUTOMATICALLY GENERATED CODE. Regenerate with genstructs.sh.
+package com.facebook.soloader;
+public final class Elf64_Ehdr {
+ public static final int e_ident = 0x0;
+ public static final int e_type = 0x10;
+ public static final int e_machine = 0x12;
+ public static final int e_version = 0x14;
+ public static final int e_entry = 0x18;
+ public static final int e_phoff = 0x20;
+ public static final int e_shoff = 0x28;
+ public static final int e_flags = 0x30;
+ public static final int e_ehsize = 0x34;
+ public static final int e_phentsize = 0x36;
+ public static final int e_phnum = 0x38;
+ public static final int e_shentsize = 0x3a;
+ public static final int e_shnum = 0x3c;
+ public static final int e_shstrndx = 0x3e;
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/soloader/Elf64_Phdr.java b/ReactAndroid/src/main/java/com/facebook/soloader/Elf64_Phdr.java
new file mode 100644
index 00000000000000..b6436cbcb08e8a
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/soloader/Elf64_Phdr.java
@@ -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.
+ */
+// AUTOMATICALLY GENERATED CODE. Regenerate with genstructs.sh.
+package com.facebook.soloader;
+public final class Elf64_Phdr {
+ public static final int p_type = 0x0;
+ public static final int p_flags = 0x4;
+ public static final int p_offset = 0x8;
+ public static final int p_vaddr = 0x10;
+ public static final int p_paddr = 0x18;
+ public static final int p_filesz = 0x20;
+ public static final int p_memsz = 0x28;
+ public static final int p_align = 0x30;
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/soloader/Elf64_Shdr.java b/ReactAndroid/src/main/java/com/facebook/soloader/Elf64_Shdr.java
new file mode 100644
index 00000000000000..36e8693d467096
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/soloader/Elf64_Shdr.java
@@ -0,0 +1,22 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+// AUTOMATICALLY GENERATED CODE. Regenerate with genstructs.sh.
+package com.facebook.soloader;
+public final class Elf64_Shdr {
+ public static final int sh_name = 0x0;
+ public static final int sh_type = 0x4;
+ public static final int sh_flags = 0x8;
+ public static final int sh_addr = 0x10;
+ public static final int sh_offset = 0x18;
+ public static final int sh_size = 0x20;
+ public static final int sh_link = 0x28;
+ public static final int sh_info = 0x2c;
+ public static final int sh_addralign = 0x30;
+ public static final int sh_entsize = 0x38;
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/soloader/ExoSoSource.java b/ReactAndroid/src/main/java/com/facebook/soloader/ExoSoSource.java
new file mode 100644
index 00000000000000..1520aa1c962ca2
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/soloader/ExoSoSource.java
@@ -0,0 +1,177 @@
+/**
+ * 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.soloader;
+
+import java.io.File;
+import java.io.IOException;
+import android.content.Context;
+
+import java.util.jar.JarFile;
+import java.util.jar.JarEntry;
+
+import java.util.regex.Pattern;
+import java.util.regex.Matcher;
+
+import android.os.Build;
+import android.system.Os;
+import android.system.ErrnoException;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Enumeration;
+
+import java.io.InputStream;
+import java.io.FileOutputStream;
+import java.io.FileInputStream;
+import java.io.BufferedReader;
+import java.io.FileReader;
+
+import android.util.Log;
+
+/**
+ * {@link SoSource} that retrieves libraries from an exopackage repository.
+ */
+public class ExoSoSource extends DirectorySoSource {
+
+ private static final String TAG = SoLoader.TAG;
+ private static final boolean DEBUG = SoLoader.DEBUG;
+
+ /**
+ * @param context Application context
+ */
+ public ExoSoSource(Context context) throws IOException {
+ //
+ // Initialize a normal DirectorySoSource that will load from our extraction directory. At this
+ // point, the directory may be empty or contain obsolete libraries, but that's okay.
+ //
+
+ super(SysUtil.createLibsDirectory(context), DirectorySoSource.RESOLVE_DEPENDENCIES);
+
+ //
+ // Synchronize the contents of that directory with the library payload in our APK, deleting and
+ // extracting as needed.
+ //
+
+ File libsDir = super.soDirectory;
+
+ if (DEBUG) {
+ Log.v(TAG, "synchronizing log directory: " + libsDir);
+ }
+
+ Map providedLibraries = findProvidedLibraries(context);
+ try (FileLocker lock = SysUtil.lockLibsDirectory(context)) {
+ // Delete files in libsDir that we don't provide or that are out of date. Forget about any
+ // libraries that are up-to-date already so we don't unpack them below.
+ File extantFiles[] = libsDir.listFiles();
+ for (int i = 0; i < extantFiles.length; ++i) {
+ File extantFile = extantFiles[i];
+
+ if (DEBUG) {
+ Log.v(TAG, "considering libdir file: " + extantFile);
+ }
+
+ String name = extantFile.getName();
+ File sourceFile = providedLibraries.get(name);
+ boolean shouldDelete =
+ (sourceFile == null ||
+ sourceFile.length() != extantFile.length() ||
+ sourceFile.lastModified() != extantFile.lastModified());
+ boolean upToDate = (sourceFile != null && !shouldDelete);
+
+ if (shouldDelete) {
+ if (DEBUG) {
+ Log.v(TAG, "deleting obsolete or unexpected file: " + extantFile);
+ }
+ SysUtil.deleteOrThrow(extantFile);
+ }
+
+ if (upToDate) {
+ if (DEBUG) {
+ Log.v(TAG, "found up-to-date library: " + extantFile);
+ }
+ providedLibraries.remove(name);
+ }
+ }
+
+ // Now extract any libraries left in providedLibraries; we removed all the up-to-date ones.
+ for (String soName : providedLibraries.keySet()) {
+ File sourceFile = providedLibraries.get(soName);
+ try (InputStream is = new FileInputStream(sourceFile)) {
+ if (DEBUG) {
+ Log.v(TAG, "extracting library: " + soName);
+ }
+ SysUtil.reliablyCopyExecutable(
+ is,
+ new File(libsDir, soName),
+ sourceFile.length(),
+ sourceFile.lastModified());
+ }
+
+ SysUtil.freeCopyBuffer();
+ }
+ }
+ }
+
+ /**
+ * Find the shared libraries provided through the exopackage directory and supported on this
+ * system. Each returend SoInfo points to the most preferred version of that library included in
+ * our exopackage directory: for example, if we're on an armv7-a system and we have both arm and
+ * armv7-a versions of libfoo, the returned entry for libfoo points to the armv7-a version of
+ * libfoo.
+ *
+ * The caller owns the returned value and may mutate it.
+ *
+ * @param context Application context
+ * @return Map of sonames to providing files
+ */
+ private static Map findProvidedLibraries(Context context) throws IOException {
+ File exoDir = new File(
+ "/data/local/tmp/exopackage/"
+ + context.getPackageName()
+ + "/native-libs/");
+
+ HashMap providedLibraries = new HashMap<>();
+ for (String abi : SysUtil.getSupportedAbis()) {
+ File abiDir = new File(exoDir, abi);
+ if (!abiDir.isDirectory()) {
+ continue;
+ }
+
+ File metadata = new File(abiDir, "metadata.txt");
+ if (!metadata.isFile()) {
+ continue;
+ }
+
+ try (FileReader fr = new FileReader(metadata);
+ BufferedReader br = new BufferedReader(fr)) {
+ String line;
+ while ((line = br.readLine()) != null) {
+ if (line.length() == 0) {
+ continue;
+ }
+
+ int sep = line.indexOf(' ');
+ if (sep == -1) {
+ throw new RuntimeException("illegal line in exopackage metadata: [" + line + "]");
+ }
+
+ String soName = line.substring(0, sep) + ".so";
+ String backingFile = line.substring(sep + 1);
+
+ if (!providedLibraries.containsKey(soName)) {
+ providedLibraries.put(soName, new File(abiDir, backingFile));
+ }
+ }
+ }
+ }
+
+ return providedLibraries;
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/soloader/FileLocker.java b/ReactAndroid/src/main/java/com/facebook/soloader/FileLocker.java
new file mode 100644
index 00000000000000..96a11f994810a8
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/soloader/FileLocker.java
@@ -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.soloader;
+import java.io.FileOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.nio.channels.FileLock;
+import java.io.Closeable;
+
+public final class FileLocker implements Closeable {
+
+ private final FileOutputStream mLockFileOutputStream;
+ private final FileLock mLock;
+
+ public static FileLocker lock(File lockFile) throws IOException {
+ return new FileLocker(lockFile);
+ }
+
+ private FileLocker(File lockFile) throws IOException {
+ mLockFileOutputStream = new FileOutputStream(lockFile);
+ FileLock lock = null;
+ try {
+ lock = mLockFileOutputStream.getChannel().lock();
+ } finally {
+ if (lock == null) {
+ mLockFileOutputStream.close();
+ }
+ }
+
+ mLock = lock;
+ }
+
+ @Override
+ public void close() throws IOException {
+ try {
+ mLock.release();
+ } finally {
+ mLockFileOutputStream.close();
+ }
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/soloader/MinElf.java b/ReactAndroid/src/main/java/com/facebook/soloader/MinElf.java
new file mode 100644
index 00000000000000..0477ad71dc996f
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/soloader/MinElf.java
@@ -0,0 +1,282 @@
+/**
+ * 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.soloader;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.File;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.channels.FileChannel;
+
+/**
+ * Extract SoLoader boottsrap information from an ELF file. This is not a general purpose ELF
+ * library.
+ *
+ * See specification at http://www.sco.com/developers/gabi/latest/contents.html. You will not be
+ * able to verify the operation of the functions below without having read the ELF specification.
+ */
+public final class MinElf {
+
+ public static final int ELF_MAGIC = 0x464c457f;
+
+ public static final int DT_NULL = 0;
+ public static final int DT_NEEDED = 1;
+ public static final int DT_STRTAB = 5;
+
+ public static final int PT_LOAD = 1;
+ public static final int PT_DYNAMIC = 2;
+
+ public static final int PN_XNUM = 0xFFFF;
+
+ public static String[] extract_DT_NEEDED(File elfFile) throws IOException {
+ FileInputStream is = new FileInputStream(elfFile);
+ try {
+ return extract_DT_NEEDED(is.getChannel());
+ } finally {
+ is.close(); // Won't throw
+ }
+ }
+
+ /**
+ * Treating {@code bb} as an ELF file, extract all the DT_NEEDED entries from its dynamic section.
+ *
+ * @param fc FileChannel referring to ELF file
+ * @return Array of strings, one for each DT_NEEDED entry, in file order
+ */
+ public static String[] extract_DT_NEEDED(FileChannel fc)
+ throws IOException {
+
+ //
+ // All constants below are fixed by the ELF specification and are the offsets of fields within
+ // the elf.h data structures.
+ //
+
+ ByteBuffer bb = ByteBuffer.allocate(8 /* largest read unit */);
+
+ // Read ELF header.
+
+ bb.order(ByteOrder.LITTLE_ENDIAN);
+ if (getu32(fc, bb, Elf32_Ehdr.e_ident) != ELF_MAGIC) {
+ throw new ElfError("file is not ELF");
+ }
+
+ boolean is32 = (getu8(fc, bb, Elf32_Ehdr.e_ident + 0x4) == 1);
+ if (getu8(fc, bb, Elf32_Ehdr.e_ident + 0x5) == 2) {
+ bb.order(ByteOrder.BIG_ENDIAN);
+ }
+
+ // Offsets above are identical in 32- and 64-bit cases.
+
+ // Find the offset of the dynamic linking information.
+
+ long e_phoff = is32
+ ? getu32(fc, bb, Elf32_Ehdr.e_phoff)
+ : get64(fc, bb, Elf64_Ehdr.e_phoff);
+
+ long e_phnum = is32
+ ? getu16(fc, bb, Elf32_Ehdr.e_phnum)
+ : getu16(fc, bb, Elf64_Ehdr.e_phnum);
+
+ int e_phentsize = is32
+ ? getu16(fc, bb, Elf32_Ehdr.e_phentsize)
+ : getu16(fc, bb, Elf64_Ehdr.e_phentsize);
+
+ if (e_phnum == PN_XNUM) { // Overflowed into section[0].sh_info
+
+ long e_shoff = is32
+ ? getu32(fc, bb, Elf32_Ehdr.e_shoff)
+ : get64(fc, bb, Elf64_Ehdr.e_shoff);
+
+ long sh_info = is32
+ ? getu32(fc, bb, e_shoff + Elf32_Shdr.sh_info)
+ : getu32(fc, bb, e_shoff + Elf64_Shdr.sh_info);
+
+ e_phnum = sh_info;
+ }
+
+ long dynStart = 0;
+ long phdr = e_phoff;
+
+ for (long i = 0; i < e_phnum; ++i) {
+ long p_type = is32
+ ? getu32(fc, bb, phdr + Elf32_Phdr.p_type)
+ : getu32(fc, bb, phdr + Elf64_Phdr.p_type);
+
+ if (p_type == PT_DYNAMIC) {
+ long p_offset = is32
+ ? getu32(fc, bb, phdr + Elf32_Phdr.p_offset)
+ : get64(fc, bb, phdr + Elf64_Phdr.p_offset);
+
+ dynStart = p_offset;
+ break;
+ }
+
+ phdr += e_phentsize;
+ }
+
+ if (dynStart == 0) {
+ throw new ElfError("ELF file does not contain dynamic linking information");
+ }
+
+ // Walk the items in the dynamic section, counting the DT_NEEDED entries. Also remember where
+ // the string table for those entries lives. That table is a pointer, which we translate to an
+ // offset below.
+
+ long d_tag;
+ int nr_DT_NEEDED = 0;
+ long dyn = dynStart;
+ long ptr_DT_STRTAB = 0;
+
+ do {
+ d_tag = is32
+ ? getu32(fc, bb, dyn + Elf32_Dyn.d_tag)
+ : get64(fc, bb, dyn + Elf64_Dyn.d_tag);
+
+ if (d_tag == DT_NEEDED) {
+ if (nr_DT_NEEDED == Integer.MAX_VALUE) {
+ throw new ElfError("malformed DT_NEEDED section");
+ }
+
+ nr_DT_NEEDED += 1;
+ } else if (d_tag == DT_STRTAB) {
+ ptr_DT_STRTAB = is32
+ ? getu32(fc, bb, dyn + Elf32_Dyn.d_un)
+ : get64(fc, bb, dyn + Elf64_Dyn.d_un);
+ }
+
+ dyn += is32 ? 8 : 16;
+ } while (d_tag != DT_NULL);
+
+ if (ptr_DT_STRTAB == 0) {
+ throw new ElfError("Dynamic section string-table not found");
+ }
+
+ // Translate the runtime string table pointer we found above to a file offset.
+
+ long off_DT_STRTAB = 0;
+ phdr = e_phoff;
+
+ for (int i = 0; i < e_phnum; ++i) {
+ long p_type = is32
+ ? getu32(fc, bb, phdr + Elf32_Phdr.p_type)
+ : getu32(fc, bb, phdr + Elf64_Phdr.p_type);
+
+ if (p_type == PT_LOAD) {
+ long p_vaddr = is32
+ ? getu32(fc, bb, phdr + Elf32_Phdr.p_vaddr)
+ : get64(fc, bb, phdr + Elf64_Phdr.p_vaddr);
+
+ long p_memsz = is32
+ ? getu32(fc, bb, phdr + Elf32_Phdr.p_memsz)
+ : get64(fc, bb, phdr + Elf64_Phdr.p_memsz);
+
+ if (p_vaddr <= ptr_DT_STRTAB && ptr_DT_STRTAB < p_vaddr + p_memsz) {
+ long p_offset = is32
+ ? getu32(fc, bb, phdr + Elf32_Phdr.p_offset)
+ : get64(fc, bb, phdr + Elf64_Phdr.p_offset);
+
+ off_DT_STRTAB = p_offset + (ptr_DT_STRTAB - p_vaddr);
+ break;
+ }
+ }
+
+ phdr += e_phentsize;
+ }
+
+ if (off_DT_STRTAB == 0) {
+ throw new ElfError("did not find file offset of DT_STRTAB table");
+ }
+
+ String[] needed = new String[nr_DT_NEEDED];
+
+ nr_DT_NEEDED = 0;
+ dyn = dynStart;
+
+ do {
+ d_tag = is32
+ ? getu32(fc, bb, dyn + Elf32_Dyn.d_tag)
+ : get64(fc, bb, dyn + Elf64_Dyn.d_tag);
+
+ if (d_tag == DT_NEEDED) {
+ long d_val = is32
+ ? getu32(fc, bb, dyn + Elf32_Dyn.d_un)
+ : get64(fc, bb, dyn + Elf64_Dyn.d_un);
+
+ needed[nr_DT_NEEDED] = getSz(fc, bb, off_DT_STRTAB + d_val);
+ if (nr_DT_NEEDED == Integer.MAX_VALUE) {
+ throw new ElfError("malformed DT_NEEDED section");
+ }
+
+ nr_DT_NEEDED += 1;
+ }
+
+ dyn += is32 ? 8 : 16;
+ } while (d_tag != DT_NULL);
+
+ if (nr_DT_NEEDED != needed.length) {
+ throw new ElfError("malformed DT_NEEDED section");
+ }
+
+ return needed;
+ }
+
+ private static String getSz(FileChannel fc, ByteBuffer bb, long offset)
+ throws IOException {
+ StringBuilder sb = new StringBuilder();
+ short b;
+ while ((b = getu8(fc, bb, offset++)) != 0) {
+ sb.append((char) b);
+ }
+
+ return sb.toString();
+ }
+
+ private static void read(FileChannel fc, ByteBuffer bb, int sz, long offset)
+ throws IOException {
+ bb.position(0);
+ bb.limit(sz);
+ if (fc.read(bb, offset) != sz) {
+ throw new ElfError("ELF file truncated");
+ }
+
+ bb.position(0);
+ }
+
+ private static long get64(FileChannel fc, ByteBuffer bb, long offset)
+ throws IOException {
+ read(fc, bb, 8, offset);
+ return bb.getLong();
+ }
+
+ private static long getu32(FileChannel fc, ByteBuffer bb, long offset)
+ throws IOException {
+ read(fc, bb, 4, offset);
+ return bb.getInt() & 0xFFFFFFFFL; // signed -> unsigned
+ }
+
+ private static int getu16(FileChannel fc, ByteBuffer bb, long offset)
+ throws IOException {
+ read(fc, bb, 2, offset);
+ return bb.getShort() & (int) 0xFFFF; // signed -> unsigned
+ }
+
+ private static short getu8(FileChannel fc, ByteBuffer bb, long offset)
+ throws IOException {
+ read(fc, bb, 1, offset);
+ return (short) (bb.get() & 0xFF); // signed -> unsigned
+ }
+
+ private static class ElfError extends RuntimeException {
+ ElfError(String why) {
+ super(why);
+ }
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/soloader/NativeLibrary.java b/ReactAndroid/src/main/java/com/facebook/soloader/NativeLibrary.java
new file mode 100644
index 00000000000000..7277474d6acce3
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/soloader/NativeLibrary.java
@@ -0,0 +1,93 @@
+/**
+ * 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.soloader;
+
+import java.util.List;
+
+import android.util.Log;
+
+/**
+ * This is the base class for all the classes representing certain native library.
+ * For loading native libraries we should always inherit from this class and provide relevant
+ * information (libraries to load, code to test native call, dependencies?).
+ *
+ * This instances should be singletons provided by DI.
+ *
+ * This is a basic template but could be improved if we find the need.
+ */
+public abstract class NativeLibrary {
+ private static final String TAG = NativeLibrary.class.getName();
+
+ private final Object mLock;
+ private List mLibraryNames;
+ private Boolean mLoadLibraries;
+ private boolean mLibrariesLoaded;
+ private volatile UnsatisfiedLinkError mLinkError;
+
+ protected NativeLibrary(List libraryNames) {
+ mLock = new Object();
+ mLoadLibraries = true;
+ mLibrariesLoaded = false;
+ mLinkError = null;
+ mLibraryNames = libraryNames;
+ }
+
+ /**
+ * safe loading of native libs
+ * @return true if native libs loaded properly, false otherwise
+ */
+ public boolean loadLibraries() {
+ synchronized (mLock) {
+ if (mLoadLibraries == false) {
+ return mLibrariesLoaded;
+ }
+ try {
+ for (String name: mLibraryNames) {
+ SoLoader.loadLibrary(name);
+ }
+ initialNativeCheck();
+ mLibrariesLoaded = true;
+ mLibraryNames = null;
+ } catch (UnsatisfiedLinkError error) {
+ Log.e(TAG, "Failed to load native lib: ", error);
+ mLinkError = error;
+ mLibrariesLoaded = false;
+ }
+ mLoadLibraries = false;
+ return mLibrariesLoaded;
+ }
+ }
+
+ /**
+ * loads libraries (if not loaded yet), throws on failure
+ * @throws UnsatisfiedLinkError
+ */
+
+ public void ensureLoaded() throws UnsatisfiedLinkError {
+ if (!loadLibraries()) {
+ throw mLinkError;
+ }
+ }
+
+ /**
+ * Override this method to make some concrete (quick and harmless) native call.
+ * This avoids lazy-loading some phones (LG) use when we call loadLibrary. If there's a problem
+ * we'll face an UnsupportedLinkError when first using the feature instead of here.
+ * This check force a check right when intended.
+ * This way clients of this library can know if it's loaded for sure or not.
+ * @throws UnsatisfiedLinkError if there was an error loading native library
+ */
+ protected void initialNativeCheck() throws UnsatisfiedLinkError {
+ }
+
+ public UnsatisfiedLinkError getError() {
+ return mLinkError;
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/soloader/NoopSoSource.java b/ReactAndroid/src/main/java/com/facebook/soloader/NoopSoSource.java
new file mode 100644
index 00000000000000..cd5d15e48eeeed
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/soloader/NoopSoSource.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+package com.facebook.soloader;
+
+import java.io.File;
+
+/**
+ * {@link SoSource} that does nothing and pretends to successfully load all libraries.
+ */
+public class NoopSoSource extends SoSource {
+ @Override
+ public int loadLibrary(String soName, int loadFlags) {
+ return LOAD_RESULT_LOADED;
+ }
+
+ @Override
+ public File unpackLibrary(String soName) {
+ throw new UnsupportedOperationException(
+ "unpacking not supported in test mode");
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/soloader/SoLoader.java b/ReactAndroid/src/main/java/com/facebook/soloader/SoLoader.java
new file mode 100644
index 00000000000000..a070ed9a96898a
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/soloader/SoLoader.java
@@ -0,0 +1,237 @@
+/**
+ * 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.soloader;
+
+import java.io.BufferedOutputStream;
+import java.io.Closeable;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import java.util.HashSet;
+import java.util.ArrayList;
+import java.io.FileNotFoundException;
+
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.os.Build;
+import android.os.StatFs;
+import android.util.Log;
+
+import android.content.pm.ApplicationInfo;
+
+/**
+ * Note that {@link com.facebook.base.app.DelegatingApplication} will automatically register itself
+ * with SoLoader before running application-specific code; most applications do not need to call
+ * {@link #init} explicitly.
+ */
+@SuppressLint({
+ "BadMethodUse-android.util.Log.v",
+ "BadMethodUse-android.util.Log.d",
+ "BadMethodUse-android.util.Log.i",
+ "BadMethodUse-android.util.Log.w",
+ "BadMethodUse-android.util.Log.e",
+})
+public class SoLoader {
+
+ /* package */ static final String TAG = "SoLoader";
+ /* package */ static final boolean DEBUG = false;
+
+ /**
+ * Ordered list of sources to consult when trying to load a shared library or one of its
+ * dependencies. {@code null} indicates that SoLoader is uninitialized.
+ */
+ @Nullable private static SoSource[] sSoSources = null;
+
+ /**
+ * Records the sonames (e.g., "libdistract.so") of shared libraries we've loaded.
+ */
+ private static final Set sLoadedLibraries = new HashSet<>();
+
+ /**
+ * Initializes native code loading for this app; this class's other static facilities cannot be
+ * used until this {@link #init} is called. This method is idempotent: calls after the first are
+ * ignored.
+ *
+ * @param context - application context.
+ * @param isNativeExopackageEnabled - whether native exopackage feature is enabled in the build.
+ */
+ public static synchronized void init(@Nullable Context context, boolean isNativeExopackageEnabled) {
+ if (sSoSources == null) {
+ ArrayList soSources = new ArrayList<>();
+
+ //
+ // Add SoSource objects for each of the system library directories.
+ //
+
+ String LD_LIBRARY_PATH = System.getenv("LD_LIBRARY_PATH");
+ if (LD_LIBRARY_PATH == null) {
+ LD_LIBRARY_PATH = "/vendor/lib:/system/lib";
+ }
+
+ String[] systemLibraryDirectories = LD_LIBRARY_PATH.split(":");
+ for (int i = 0; i < systemLibraryDirectories.length; ++i) {
+ // Don't pass DirectorySoSource.RESOLVE_DEPENDENCIES for directories we find on
+ // LD_LIBRARY_PATH: Bionic's dynamic linker is capable of correctly resolving dependencies
+ // these libraries have on each other, so doing that ourselves would be a waste.
+ File systemSoDirectory = new File(systemLibraryDirectories[i]);
+ soSources.add(
+ new DirectorySoSource(
+ systemSoDirectory,
+ DirectorySoSource.ON_LD_LIBRARY_PATH));
+ }
+
+ //
+ // We can only proceed forward if we have a Context. The prominent case
+ // where we don't have a Context is barebones dalvikvm instantiations. In
+ // that case, the caller is responsible for providing a correct LD_LIBRARY_PATH.
+ //
+
+ if (context != null) {
+ //
+ // Prepend our own SoSource for our own DSOs.
+ //
+
+ ApplicationInfo applicationInfo = context.getApplicationInfo();
+ boolean isSystemApplication =
+ (applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0 &&
+ (applicationInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) == 0;
+
+ try {
+ if (isNativeExopackageEnabled) {
+ soSources.add(0, new ExoSoSource(context));
+ } else if (isSystemApplication) {
+ soSources.add(0, new ApkSoSource(context));
+ } else {
+ // Delete the old libs directory if we don't need it.
+ SysUtil.dumbDeleteRecrusive(SysUtil.getLibsDirectory(context));
+
+ int ourSoSourceFlags = 0;
+
+ // On old versions of Android, Bionic doesn't add our library directory to its internal
+ // search path, and the system doesn't resolve dependencies between modules we ship. On
+ // these systems, we resolve dependencies ourselves. On other systems, Bionic's built-in
+ // resolver suffices.
+
+ if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.JELLY_BEAN_MR1) {
+ ourSoSourceFlags |= DirectorySoSource.RESOLVE_DEPENDENCIES;
+ }
+
+ SoSource ourSoSource = new DirectorySoSource(
+ new File(applicationInfo.nativeLibraryDir),
+ ourSoSourceFlags);
+
+ soSources.add(0, ourSoSource);
+ }
+ } catch (IOException ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+
+ sSoSources = soSources.toArray(new SoSource[soSources.size()]);
+ }
+ }
+
+ /**
+ * Turn shared-library loading into a no-op. Useful in special circumstances.
+ */
+ public static void setInTestMode() {
+ sSoSources = new SoSource[]{new NoopSoSource()};
+ }
+
+ /**
+ * Load a shared library, initializing any JNI binding it contains.
+ *
+ * @param shortName Name of library to find, without "lib" prefix or ".so" suffix
+ */
+ public static synchronized void loadLibrary(String shortName)
+ throws UnsatisfiedLinkError
+ {
+ if (sSoSources == null) {
+ // This should never happen during normal operation,
+ // but if we're running in a non-Android environment,
+ // fall back to System.loadLibrary.
+ if ("http://www.android.com/".equals(System.getProperty("java.vendor.url"))) {
+ // This will throw.
+ assertInitialized();
+ } else {
+ // Not on an Android system. Ask the JVM to load for us.
+ System.loadLibrary(shortName);
+ return;
+ }
+ }
+
+ try {
+ loadLibraryBySoName(System.mapLibraryName(shortName), 0);
+ } catch (IOException ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+
+ /**
+ * Unpack library and its dependencies, returning the location of the unpacked library file. All
+ * non-system dependencies of the given library will either be on LD_LIBRARY_PATH or will be in
+ * the same directory as the returned File.
+ *
+ * @param shortName Name of library to find, without "lib" prefix or ".so" suffix
+ * @return Unpacked DSO location
+ */
+ public static File unpackLibraryAndDependencies(String shortName)
+ throws UnsatisfiedLinkError
+ {
+ assertInitialized();
+ try {
+ return unpackLibraryBySoName(System.mapLibraryName(shortName));
+ } catch (IOException ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+
+ /* package */ static void loadLibraryBySoName(String soName, int loadFlags) throws IOException {
+ int result = sLoadedLibraries.contains(soName)
+ ? SoSource.LOAD_RESULT_LOADED
+ : SoSource.LOAD_RESULT_NOT_FOUND;
+
+ for (int i = 0; result == SoSource.LOAD_RESULT_NOT_FOUND && i < sSoSources.length; ++i) {
+ result = sSoSources[i].loadLibrary(soName, loadFlags);
+ }
+
+ if (result == SoSource.LOAD_RESULT_NOT_FOUND) {
+ throw new UnsatisfiedLinkError("could find DSO to load: " + soName);
+ }
+
+ if (result == SoSource.LOAD_RESULT_LOADED) {
+ sLoadedLibraries.add(soName);
+ }
+ }
+
+ /* package */ static File unpackLibraryBySoName(String soName) throws IOException {
+ for (int i = 0; i < sSoSources.length; ++i) {
+ File unpacked = sSoSources[i].unpackLibrary(soName);
+ if (unpacked != null) {
+ return unpacked;
+ }
+ }
+
+ throw new FileNotFoundException(soName);
+ }
+
+ private static void assertInitialized() {
+ if (sSoSources == null) {
+ throw new RuntimeException("SoLoader.init() not yet called");
+ }
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/soloader/SoSource.java b/ReactAndroid/src/main/java/com/facebook/soloader/SoSource.java
new file mode 100644
index 00000000000000..016013e15af73a
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/soloader/SoSource.java
@@ -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.soloader;
+
+import java.io.File;
+import java.io.IOException;
+
+abstract public class SoSource {
+
+ /**
+ * This SoSource doesn't know how to provide the given library.
+ */
+ public static final int LOAD_RESULT_NOT_FOUND = 0;
+
+ /**
+ * This SoSource loaded the given library.
+ */
+ public static final int LOAD_RESULT_LOADED = 1;
+
+ /**
+ * This SoSource did not load the library, but verified that the system loader will load it if
+ * some other library depends on it. Returned only if LOAD_FLAG_ALLOW_IMPLICIT_PROVISION is
+ * provided to loadLibrary.
+ */
+ public static final int LOAD_RESULT_IMPLICITLY_PROVIDED = 2;
+
+ /**
+ * Allow loadLibrary to implicitly provide the library instead of actually loading it.
+ */
+ public static final int LOAD_FLAG_ALLOW_IMPLICIT_PROVISION = 1;
+
+ /**
+ * Load a shared library library into this process. This routine is independent of
+ * {@link #loadLibrary}.
+ *
+ * @param soName Name of library to load
+ * @param loadFlags Zero or more of the LOAD_FLAG_XXX constants.
+ * @return One of the LOAD_RESULT_XXX constants.
+ */
+ abstract public int loadLibrary(String soName, int LoadFlags) throws IOException;
+
+ /**
+ * Ensure that a shared library exists on disk somewhere. This routine is independent of
+ * {@link #loadLibrary}.
+ *
+ * @param soName Name of library to load
+ * @return File if library found; {@code null} if not.
+ */
+ abstract public File unpackLibrary(String soName) throws IOException;
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/soloader/SysUtil.java b/ReactAndroid/src/main/java/com/facebook/soloader/SysUtil.java
new file mode 100644
index 00000000000000..91f28583e1469c
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/soloader/SysUtil.java
@@ -0,0 +1,205 @@
+/**
+ * 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.soloader;
+
+import java.io.File;
+import java.io.IOException;
+import android.content.Context;
+
+import java.util.jar.JarFile;
+import java.util.jar.JarEntry;
+
+import java.util.regex.Pattern;
+import java.util.regex.Matcher;
+
+import android.os.Build;
+import android.system.Os;
+import android.system.ErrnoException;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Enumeration;
+
+import java.io.InputStream;
+import java.io.FileOutputStream;
+import java.io.FileDescriptor;
+
+/*package*/ final class SysUtil {
+
+ private static byte[] cachedBuffer = null;
+
+ /**
+ * Copy from an inputstream to a named filesystem file. Take care to ensure that we can detect
+ * incomplete copies and that the copied bytes make it to stable storage before returning.
+ * The destination file will be marked executable.
+ *
+ * This routine caches an internal buffer between invocations; after making a sequence of calls
+ * {@link #reliablyCopyExecutable} calls, call {@link #freeCopyBuffer} to release this buffer.
+ *
+ * @param is Stream from which to copy
+ * @param destination File to which to write
+ * @param expectedSize Number of bytes we expect to write; -1 if unknown
+ * @param time Modification time to which to set file on success; must be in the past
+ */
+ public static void reliablyCopyExecutable(
+ InputStream is,
+ File destination,
+ long expectedSize,
+ long time) throws IOException {
+ destination.delete();
+ try (FileOutputStream os = new FileOutputStream(destination)) {
+ byte buffer[];
+ if (cachedBuffer == null) {
+ cachedBuffer = buffer = new byte[16384];
+ } else {
+ buffer = cachedBuffer;
+ }
+
+ int nrBytes;
+ if (expectedSize > 0) {
+ fallocateIfSupported(os.getFD(), expectedSize);
+ }
+
+ while ((nrBytes = is.read(buffer, 0, buffer.length)) >= 0) {
+ os.write(buffer, 0, nrBytes);
+ }
+
+ os.getFD().sync();
+ destination.setExecutable(true);
+ destination.setLastModified(time);
+ os.getFD().sync();
+ }
+ }
+
+ /**
+ * Free the internal buffer cache for {@link #reliablyCopyExecutable}.
+ */
+ public static void freeCopyBuffer() {
+ cachedBuffer = null;
+ }
+
+ /**
+ * Determine how preferred a given ABI is on this system.
+ *
+ * @param supportedAbis ABIs on this system
+ * @param abi ABI of a shared library we might want to unpack
+ * @return -1 if not supported or an integer, smaller being more preferred
+ */
+ public static int findAbiScore(String[] supportedAbis, String abi) {
+ for (int i = 0; i < supportedAbis.length; ++i) {
+ if (supportedAbis[i] != null && abi.equals(supportedAbis[i])) {
+ return i;
+ }
+ }
+
+ return -1;
+ }
+
+ public static void deleteOrThrow(File file) throws IOException {
+ if (!file.delete()) {
+ throw new IOException("could not delete file " + file);
+ }
+ }
+
+ /**
+ * Return an list of ABIs we supported on this device ordered according to preference. Use a
+ * separate inner class to isolate the version-dependent call where it won't cause the whole
+ * class to fail preverification.
+ *
+ * @return Ordered array of supported ABIs
+ */
+ public static String[] getSupportedAbis() {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
+ return new String[]{Build.CPU_ABI, Build.CPU_ABI2};
+ } else {
+ return LollipopSysdeps.getSupportedAbis();
+ }
+ }
+
+ /**
+ * Pre-allocate disk space for a file if we can do that
+ * on this version of the OS.
+ *
+ * @param fd File descriptor for file
+ * @param length Number of bytes to allocate.
+ */
+ public static void fallocateIfSupported(FileDescriptor fd, long length) throws IOException {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ LollipopSysdeps.fallocate(fd, length);
+ }
+ }
+
+ public static FileLocker lockLibsDirectory(Context context) throws IOException {
+ File lockFile = new File(context.getApplicationInfo().dataDir, "libs-dir-lock");
+ return FileLocker.lock(lockFile);
+ }
+
+ /**
+ * Return the directory into which we put our self-extracted native libraries.
+ *
+ * @param context Application context
+ * @return File pointing to an existing directory
+ */
+ /* package */ static File getLibsDirectory(Context context) {
+ return new File(context.getApplicationInfo().dataDir, "app_libs");
+ }
+
+ /**
+ * Return the directory into which we put our self-extracted native libraries and make sure it
+ * exists.
+ */
+ /* package */ static File createLibsDirectory(Context context) {
+ File libsDirectory = getLibsDirectory(context);
+ if (!libsDirectory.isDirectory() && !libsDirectory.mkdirs()) {
+ throw new RuntimeException("could not create libs directory");
+ }
+
+ return libsDirectory;
+ }
+
+ /**
+ * Delete a directory and its contents.
+ *
+ * WARNING: Java APIs do not let us distinguish directories from symbolic links to directories.
+ * Consequently, if the directory contains symbolic links to directories, we will attempt to
+ * delete the contents of pointed-to directories.
+ *
+ * @param file File or directory to delete
+ */
+ /* package */ static void dumbDeleteRecrusive(File file) throws IOException {
+ if (file.isDirectory()) {
+ for (File entry : file.listFiles()) {
+ dumbDeleteRecrusive(entry);
+ }
+ }
+
+ if (!file.delete() && file.exists()) {
+ throw new IOException("could not delete: " + file);
+ }
+ }
+
+ /**
+ * Encapsulate Lollipop-specific calls into an independent class so we don't fail preverification
+ * downlevel.
+ */
+ private static final class LollipopSysdeps {
+ public static String[] getSupportedAbis() {
+ return Build.SUPPORTED_32_BIT_ABIS; // We ain't doing no newfangled 64-bit
+ }
+
+ public static void fallocate(FileDescriptor fd, long length) throws IOException {
+ try {
+ Os.posix_fallocate(fd, 0, length);
+ } catch (ErrnoException ex) {
+ throw new IOException(ex.toString(), ex);
+ }
+ }
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/soloader/genstructs.sh b/ReactAndroid/src/main/java/com/facebook/soloader/genstructs.sh
new file mode 100644
index 00000000000000..a7bcd49a581a82
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/soloader/genstructs.sh
@@ -0,0 +1,35 @@
+#!/bin/bash
+
+#
+# This script generates Java structures that contain the offsets of
+# fields in various ELF ABI structures. com.facebook.soloader.MinElf
+# uses these structures while parsing ELF files.
+#
+
+set -euo pipefail
+
+struct2java() {
+ ../../../../scripts/struct2java.py "$@"
+}
+
+declare -a structs=(Elf32_Ehdr Elf64_Ehdr)
+structs+=(Elf32_Ehdr Elf64_Ehdr)
+structs+=(Elf32_Phdr Elf64_Phdr)
+structs+=(Elf32_Shdr Elf64_Shdr)
+structs+=(Elf32_Dyn Elf64_Dyn)
+
+for struct in "${structs[@]}"; do
+ cat > elfhdr.c <
+static const $struct a;
+EOF
+ gcc -g -c -o elfhdr.o elfhdr.c
+ cat > $struct.java <> $struct.java
+done
+
+rm -f elfhdr.o elfhdr.c
diff --git a/ReactAndroid/src/main/java/com/facebook/soloader/soloader.pro b/ReactAndroid/src/main/java/com/facebook/soloader/soloader.pro
new file mode 100644
index 00000000000000..4a832314c5492e
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/soloader/soloader.pro
@@ -0,0 +1,6 @@
+# Ensure that methods from LollipopSysdeps don't get inlined. LollipopSysdeps.fallocate references
+# an exception that isn't present prior to Lollipop, which trips up the verifier if the class is
+# loaded on a pre-Lollipop OS.
+-keep class com.facebook.soloader.SysUtil$LollipopSysdeps {
+ public ;
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/systrace/Systrace.java b/ReactAndroid/src/main/java/com/facebook/systrace/Systrace.java
new file mode 100644
index 00000000000000..dd523375b46edf
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/systrace/Systrace.java
@@ -0,0 +1,31 @@
+/**
+ * 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.systrace;
+
+
+/**
+ * Systrace stub.
+ */
+public class Systrace {
+
+ public static final long TRACE_TAG_REACT_JAVA_BRIDGE = 0L;
+
+ public static void beginSection(long tag, final String sectionName) {
+ }
+
+ public static void endSection(long tag) {
+ }
+
+ public static void traceCounter(
+ long tag,
+ final String counterName,
+ final int counterValue) {
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/systrace/SystraceMessage.java b/ReactAndroid/src/main/java/com/facebook/systrace/SystraceMessage.java
new file mode 100644
index 00000000000000..3a255eb940047b
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/systrace/SystraceMessage.java
@@ -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.systrace;
+
+/**
+ * Systrace stub.
+ */
+public final class SystraceMessage {
+
+ private static final Builder NOOP_BUILDER = new NoopBuilder();
+
+ public static Builder beginSection(long tag, String sectionName) {
+ return NOOP_BUILDER;
+ }
+
+ public static Builder endSection(long tag) {
+ return NOOP_BUILDER;
+ }
+
+ public static abstract class Builder {
+
+ public abstract void flush();
+
+ public abstract Builder arg(String key, Object value);
+
+ public abstract Builder arg(String key, int value);
+
+ public abstract Builder arg(String key, long value);
+
+ public abstract Builder arg(String key, double value);
+ }
+
+ private interface Flusher {
+ void flush(StringBuilder builder);
+ }
+
+ private static class NoopBuilder extends Builder {
+ @Override
+ public void flush() {
+ }
+
+ @Override
+ public Builder arg(String key, Object value) {
+ return this;
+ }
+
+ @Override
+ public Builder arg(String key, int value) {
+ return this;
+ }
+
+ @Override
+ public Builder arg(String key, long value) {
+ return this;
+ }
+
+ @Override
+ public Builder arg(String key, double value) {
+ return this;
+ }
+ }
+}
diff --git a/ReactAndroid/src/main/jni/Application.mk b/ReactAndroid/src/main/jni/Application.mk
new file mode 100644
index 00000000000000..d8f9dda846a371
--- /dev/null
+++ b/ReactAndroid/src/main/jni/Application.mk
@@ -0,0 +1,14 @@
+APP_BUILD_SCRIPT := Android.mk
+
+APP_ABI := armeabi-v7a x86
+APP_PLATFORM := android-9
+
+APP_MK_DIR := $(dir $(lastword $(MAKEFILE_LIST)))
+NDK_MODULE_PATH := $(APP_MK_DIR):$(THIRD_PARTY_NDK_DIR):$(APP_MK_DIR)/first-party
+
+APP_STL := gnustl_shared
+
+# Make sure every shared lib includes a .note.gnu.build-id header
+APP_LDFLAGS := -Wl,--build-id
+
+NDK_TOOLCHAIN_VERSION := 4.8
diff --git a/ReactAndroid/src/main/jni/first-party/fb/Android.mk b/ReactAndroid/src/main/jni/first-party/fb/Android.mk
new file mode 100644
index 00000000000000..3361c433d28ce5
--- /dev/null
+++ b/ReactAndroid/src/main/jni/first-party/fb/Android.mk
@@ -0,0 +1,30 @@
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_SRC_FILES:= \
+ assert.cpp \
+ log.cpp \
+
+LOCAL_C_INCLUDES := $(LOCAL_PATH)/.. $(LOCAL_PATH)/include
+LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/.. $(LOCAL_PATH)/include
+
+LOCAL_CFLAGS := -DLOG_TAG=\"libfb\"
+LOCAL_CFLAGS += -Wall -Werror
+# include/utils/threads.h has unused parameters
+LOCAL_CFLAGS += -Wno-unused-parameter
+ifeq ($(TOOLCHAIN_PERMISSIVE),true)
+ LOCAL_CFLAGS += -Wno-error=unused-but-set-variable
+endif
+LOCAL_CFLAGS += -DHAVE_POSIX_CLOCKS
+
+CXX11_FLAGS := -std=c++11
+LOCAL_CFLAGS += $(CXX11_FLAGS)
+
+LOCAL_EXPORT_CPPFLAGS := $(CXX11_FLAGS)
+
+LOCAL_LDLIBS := -llog -ldl -landroid
+LOCAL_EXPORT_LDLIBS := -llog
+
+LOCAL_MODULE := libfb
+
+include $(BUILD_SHARED_LIBRARY)
\ No newline at end of file
diff --git a/ReactAndroid/src/main/jni/first-party/fb/Countable.h b/ReactAndroid/src/main/jni/first-party/fb/Countable.h
new file mode 100644
index 00000000000000..1e402a3fcb2345
--- /dev/null
+++ b/ReactAndroid/src/main/jni/first-party/fb/Countable.h
@@ -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.
+ */
+
+#pragma once
+#include
+#include
+#include
+#include
+#include
+
+namespace facebook {
+
+class Countable : public noncopyable, public nonmovable {
+public:
+ // RefPtr expects refcount to start at 0
+ Countable() : m_refcount(0) {}
+ virtual ~Countable()
+ {
+ FBASSERT(m_refcount == 0);
+ }
+
+private:
+ void ref() {
+ ++m_refcount;
+ }
+
+ void unref() {
+ if (0 == --m_refcount) {
+ delete this;
+ }
+ }
+
+ bool hasOnlyOneRef() const {
+ return m_refcount == 1;
+ }
+
+ template friend class RefPtr;
+ std::atomic m_refcount;
+};
+
+}
diff --git a/ReactAndroid/src/main/jni/first-party/fb/ProgramLocation.h b/ReactAndroid/src/main/jni/first-party/fb/ProgramLocation.h
new file mode 100644
index 00000000000000..36f7737f643684
--- /dev/null
+++ b/ReactAndroid/src/main/jni/first-party/fb/ProgramLocation.h
@@ -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.
+ */
+
+#pragma once
+#include
+#include
+#include
+
+namespace facebook {
+
+#define FROM_HERE facebook::ProgramLocation(__FUNCTION__, __FILE__, __LINE__)
+
+class ProgramLocation {
+public:
+ ProgramLocation() : m_functionName("Unspecified"), m_fileName("Unspecified"), m_lineNumber(0) {}
+
+ ProgramLocation(const char* functionName, const char* fileName, int line) :
+ m_functionName(functionName),
+ m_fileName(fileName),
+ m_lineNumber(line)
+ {}
+
+ const char* functionName() const { return m_functionName; }
+ const char* fileName() const { return m_fileName; }
+ int lineNumber() const { return m_lineNumber; }
+
+ std::string asFormattedString() const {
+ std::stringstream str;
+ str << "Function " << m_functionName << " in file " << m_fileName << ":" << m_lineNumber;
+ return str.str();
+ }
+
+ bool operator==(const ProgramLocation& other) const {
+ // Assumes that the strings are static
+ return (m_functionName == other.m_functionName) && (m_fileName == other.m_fileName) && m_lineNumber == other.m_lineNumber;
+ }
+
+private:
+ const char* m_functionName;
+ const char* m_fileName;
+ int m_lineNumber;
+};
+
+}
diff --git a/ReactAndroid/src/main/jni/first-party/fb/RefPtr.h b/ReactAndroid/src/main/jni/first-party/fb/RefPtr.h
new file mode 100644
index 00000000000000..d21fe697ea5adc
--- /dev/null
+++ b/ReactAndroid/src/main/jni/first-party/fb/RefPtr.h
@@ -0,0 +1,274 @@
+/*
+ * 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.
+ */
+
+#pragma once
+#include
+#include
+
+namespace facebook {
+
+// Reference counting smart pointer. This is designed to work with the
+// Countable class or other implementations in the future. It is designed in a
+// way to be both efficient and difficult to misuse. Typical usage is very
+// simple once you learn the patterns (and the compiler will help!):
+//
+// By default, the internal pointer is null.
+// RefPtr ref;
+//
+// Object creation requires explicit construction:
+// RefPtr ref = createNew(...);
+//
+// Or if the constructor is not public:
+// RefPtr ref = adoptRef(new Foo(...));
+//
+// But you can implicitly create from nullptr:
+// RefPtr maybeRef = cond ? ref : nullptr;
+//
+// Move/Copy Construction/Assignment are straightforward:
+// RefPtr ref2 = ref;
+// ref = std::move(ref2);
+//
+// Destruction automatically drops the RefPtr's reference as expected.
+//
+// Upcasting is implicit but downcasting requires an explicit cast:
+// struct Bar : public Foo {};
+// RefPtr barRef = static_cast>(ref);
+// ref = barRef;
+//
+template
+class RefPtr {
+public:
+ constexpr RefPtr() :
+ m_ptr(nullptr)
+ {}
+
+ // Allow implicit construction from a pointer only from nullptr
+ constexpr RefPtr(std::nullptr_t ptr) :
+ m_ptr(nullptr)
+ {}
+
+ RefPtr(const RefPtr& ref) :
+ m_ptr(ref.m_ptr)
+ {
+ refIfNecessary(m_ptr);
+ }
+
+ // Only allow implicit upcasts. A downcast will result in a compile error
+ // unless you use static_cast (which will end up invoking the explicit
+ // operator below).
+ template
+ RefPtr(const RefPtr& ref, typename std::enable_if::value, U>::type* = nullptr) :
+ m_ptr(ref.get())
+ {
+ refIfNecessary(m_ptr);
+ }
+
+ RefPtr(RefPtr&& ref) :
+ m_ptr(nullptr)
+ {
+ *this = std::move(ref);
+ }
+
+ // Only allow implicit upcasts. A downcast will result in a compile error
+ // unless you use static_cast (which will end up invoking the explicit
+ // operator below).
+ template
+ RefPtr(RefPtr&& ref, typename std::enable_if::value, U>::type* = nullptr) :
+ m_ptr(nullptr)
+ {
+ *this = std::move(ref);
+ }
+
+ ~RefPtr() {
+ unrefIfNecessary(m_ptr);
+ m_ptr = nullptr;
+ }
+
+ RefPtr& operator=(const RefPtr& ref) {
+ if (m_ptr != ref.m_ptr) {
+ unrefIfNecessary(m_ptr);
+ m_ptr = ref.m_ptr;
+ refIfNecessary(m_ptr);
+ }
+ return *this;
+ }
+
+ // The STL assumes rvalue references are unique and for simplicity's sake, we
+ // make the same assumption here, that &ref != this.
+ RefPtr& operator=(RefPtr&& ref) {
+ unrefIfNecessary(m_ptr);
+ m_ptr = ref.m_ptr;
+ ref.m_ptr = nullptr;
+ return *this;
+ }
+
+ template
+ RefPtr& operator=(RefPtr&& ref) {
+ unrefIfNecessary(m_ptr);
+ m_ptr = ref.m_ptr;
+ ref.m_ptr = nullptr;
+ return *this;
+ }
+
+ void reset() {
+ unrefIfNecessary(m_ptr);
+ m_ptr = nullptr;
+ }
+
+ T* get() const {
+ return m_ptr;
+ }
+
+ T* operator->() const {
+ return m_ptr;
+ }
+
+ T& operator*() const {
+ return *m_ptr;
+ }
+
+ template
+ explicit operator RefPtr () const;
+
+ explicit operator bool() const {
+ return m_ptr ? true : false;
+ }
+
+ bool isTheLastRef() const {
+ FBASSERT(m_ptr);
+ return m_ptr->hasOnlyOneRef();
+ }
+
+ // Creates a strong reference from a raw pointer, assuming that is already
+ // referenced from some other RefPtr. This should be used sparingly.
+ static inline RefPtr assumeAlreadyReffed(T* ptr) {
+ return RefPtr(ptr, ConstructionMode::External);
+ }
+
+ // Creates a strong reference from a raw pointer, assuming that it points to a
+ // freshly-created object. See the documentation for RefPtr for usage.
+ static inline RefPtr adoptRef(T* ptr) {
+ return RefPtr(ptr, ConstructionMode::Adopted);
+ }
+
+private:
+ enum class ConstructionMode {
+ Adopted,
+ External
+ };
+
+ RefPtr(T* ptr, ConstructionMode mode) :
+ m_ptr(ptr)
+ {
+ FBASSERTMSGF(ptr, "Got null pointer in %s construction mode", mode == ConstructionMode::Adopted ? "adopted" : "external");
+ ptr->ref();
+ if (mode == ConstructionMode::Adopted) {
+ FBASSERT(ptr->hasOnlyOneRef());
+ }
+ }
+
+ static inline void refIfNecessary(T* ptr) {
+ if (ptr) {
+ ptr->ref();
+ }
+ }
+ static inline void unrefIfNecessary(T* ptr) {
+ if (ptr) {
+ ptr->unref();
+ }
+ }
+
+ template friend class RefPtr;
+
+ T* m_ptr;
+};
+
+// Creates a strong reference from a raw pointer, assuming that is already
+// referenced from some other RefPtr and that it is non-null. This should be
+// used sparingly.
+template
+static inline RefPtr assumeAlreadyReffed(T* ptr) {
+ return RefPtr::assumeAlreadyReffed(ptr);
+}
+
+// As above, but tolerant of nullptr.
+template
+static inline RefPtr assumeAlreadyReffedOrNull(T* ptr) {
+ return ptr ? RefPtr::assumeAlreadyReffed(ptr) : nullptr;
+}
+
+// Creates a strong reference from a raw pointer, assuming that it points to a
+// freshly-created object. See the documentation for RefPtr for usage.
+template
+static inline RefPtr adoptRef(T* ptr) {
+ return RefPtr::adoptRef(ptr);
+}
+
+template
+static inline RefPtr createNew(Args&&... arguments) {
+ return RefPtr::adoptRef(new T(std::forward(arguments)...));
+}
+
+template template
+RefPtr::operator RefPtr() const {
+ static_assert(std::is_base_of::value, "Invalid static cast");
+ return assumeAlreadyReffedOrNull(static_cast(m_ptr));
+}
+
+template
+inline bool operator==(const RefPtr& a, const RefPtr& b) {
+ return a.get() == b.get();
+}
+
+template
+inline bool operator!=(const RefPtr& a, const RefPtr& b) {
+ return a.get() != b.get();
+}
+
+template
+inline bool operator==(const RefPtr& ref, U* ptr) {
+ return ref.get() == ptr;
+}
+
+template
+inline bool operator!=(const RefPtr& ref, U* ptr) {
+ return ref.get() != ptr;
+}
+
+template
+inline bool operator==(U* ptr, const RefPtr& ref) {
+ return ref.get() == ptr;
+}
+
+template
+inline bool operator!=(U* ptr, const RefPtr& ref) {
+ return ref.get() != ptr;
+}
+
+template
+inline bool operator==(const RefPtr& ref, std::nullptr_t ptr) {
+ return ref.get() == ptr;
+}
+
+template
+inline bool operator!=(const RefPtr& ref, std::nullptr_t ptr) {
+ return ref.get() != ptr;
+}
+
+template
+inline bool operator==(std::nullptr_t ptr, const RefPtr& ref) {
+ return ref.get() == ptr;
+}
+
+template
+inline bool operator!=(std::nullptr_t ptr, const RefPtr& ref) {
+ return ref.get() != ptr;
+}
+
+}
diff --git a/ReactAndroid/src/main/jni/first-party/fb/StaticInitialized.h b/ReactAndroid/src/main/jni/first-party/fb/StaticInitialized.h
new file mode 100644
index 00000000000000..6d943972a61f94
--- /dev/null
+++ b/ReactAndroid/src/main/jni/first-party/fb/StaticInitialized.h
@@ -0,0 +1,40 @@
+/*
+ * 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.
+ */
+
+#pragma once
+#include
+#include
+
+namespace facebook {
+
+// Class that lets you declare a global but does not add a static constructor
+// to the binary. Eventually I'd like to have this auto-initialize in a
+// multithreaded environment but for now it's easiest just to use manual
+// initialization.
+template
+class StaticInitialized {
+public:
+ constexpr StaticInitialized() :
+ m_instance(nullptr)
+ {}
+
+ template
+ void initialize(Args&&... arguments) {
+ FBASSERT(!m_instance);
+ m_instance = new T(std::forward(arguments)...);
+ }
+
+ T* operator->() const {
+ return m_instance;
+ }
+private:
+ T* m_instance;
+};
+
+}
diff --git a/ReactAndroid/src/main/jni/first-party/fb/ThreadLocal.h b/ReactAndroid/src/main/jni/first-party/fb/ThreadLocal.h
new file mode 100644
index 00000000000000..d86a2f0deac66d
--- /dev/null
+++ b/ReactAndroid/src/main/jni/first-party/fb/ThreadLocal.h
@@ -0,0 +1,118 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include
+#include
+
+#include
+
+namespace facebook {
+
+///////////////////////////////////////////////////////////////////////////////
+
+/**
+ * A thread-local object is a "global" object within a thread. This is useful
+ * for writing apartment-threaded code, where nothing is actullay shared
+ * between different threads (hence no locking) but those variables are not
+ * on stack in local scope. To use it, just do something like this,
+ *
+ * ThreadLocal static_object;
+ * static_object->data_ = ...;
+ * static_object->doSomething();
+ *
+ * ThreadLocal